From 73104b44d3671e323aeed8ef642f5ff62651210b Mon Sep 17 00:00:00 2001 From: Georgi Petroff <102983346+92Infinitus92@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:52:51 +0300 Subject: [PATCH 01/13] feat(scenarios): add Pump support - state templates & presets (#767) --- .gitignore | 1 + crates/core/Cargo.toml | 1 + crates/core/src/lib.rs | 2 + crates/core/src/rpc/surfnet_cheatcodes.rs | 228 +- crates/core/src/scenarios/README.md | 2 + crates/core/src/scenarios/mod.rs | 1 + crates/core/src/scenarios/protocols/mod.rs | 1 + .../scenarios/protocols/pump-amm/README.md | 65 + .../scenarios/protocols/pump-amm/v1/idl.json | 7390 +++++++++++++++++ .../protocols/pump-amm/v1/overrides.yaml | 165 + .../src/scenarios/protocols/pump/README.md | 209 + .../core/src/scenarios/protocols/pump/mod.rs | 1 + .../protocols/pump/v1/graduation_builder.rs | 406 + .../src/scenarios/protocols/pump/v1/idl.json | 6813 +++++++++++++++ .../src/scenarios/protocols/pump/v1/mod.rs | 1 + .../protocols/pump/v1/overrides.yaml | 164 + crates/core/src/scenarios/registry.rs | 135 +- crates/core/src/surfnet/svm.rs | 279 +- crates/core/src/tests/integration.rs | 9 +- crates/core/src/tests/mod.rs | 2 + crates/core/src/tests/pump/mod.rs | 1102 +++ crates/core/src/types.rs | 84 + crates/mcp/src/surfpool/mod.rs | 520 +- crates/types/src/scenarios.rs | 24 +- crates/types/src/verified_tokens.rs | 23 +- 25 files changed, 17499 insertions(+), 129 deletions(-) create mode 100644 crates/core/src/scenarios/protocols/mod.rs create mode 100644 crates/core/src/scenarios/protocols/pump-amm/README.md create mode 100644 crates/core/src/scenarios/protocols/pump-amm/v1/idl.json create mode 100644 crates/core/src/scenarios/protocols/pump-amm/v1/overrides.yaml create mode 100644 crates/core/src/scenarios/protocols/pump/README.md create mode 100644 crates/core/src/scenarios/protocols/pump/mod.rs create mode 100644 crates/core/src/scenarios/protocols/pump/v1/graduation_builder.rs create mode 100644 crates/core/src/scenarios/protocols/pump/v1/idl.json create mode 100644 crates/core/src/scenarios/protocols/pump/v1/mod.rs create mode 100644 crates/core/src/scenarios/protocols/pump/v1/overrides.yaml create mode 100644 crates/core/src/tests/pump/mod.rs diff --git a/.gitignore b/.gitignore index 360c85527..ddf6f4854 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target .DS_Store +.idea .cache test-ledger diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 4398fed91..dc64e9a61 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -129,5 +129,6 @@ sbpf-debugger = ["litesvm/sbpf-debugger"] sqlite = ["surfpool-db/sqlite"] postgres = ["surfpool-db/postgres"] ignore_tests_ci = [] +integration-tests = [] register-tracing = ["litesvm/register-tracing"] prometheus = ["dep:opentelemetry", "dep:opentelemetry_sdk", "dep:opentelemetry-prometheus", "dep:prometheus", "dep:axum"] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index a250e8271..34eb1a7cc 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -25,6 +25,8 @@ use crossbeam_channel::{Receiver, Sender}; pub use jsonrpc_core; pub use jsonrpc_http_server; pub use litesvm; +pub use solana_account; +pub use solana_commitment_config; use solana_pubkey::Pubkey; pub use solana_rpc_client; use surfnet::{GeyserEvent, locker::SurfnetSvmLocker, svm::SurfnetSvm}; diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index 6ee560a16..7f632e86d 100644 --- a/crates/core/src/rpc/surfnet_cheatcodes.rs +++ b/crates/core/src/rpc/surfnet_cheatcodes.rs @@ -1858,14 +1858,31 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { .inner .minimum_balance_for_rent_exemption(data.len()) }); - (data, rent) + (data, Some(rent)) } else { - (token_account_data.pack_into_vec(), initial_lamports) + let data = token_account_data + .pack_into_preserving_extensions(token_account.expected_data()) + .map_err(|e| { + Error::invalid_params(format!("Failed to pack token account data: {}", e)) + })?; + // Wrapped SOL backs the post-update amount with lamports on top + // of the rent floor for the account's actual size, extensions + // included. + let lamports = is_native_mint.then(|| { + svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .minimum_balance_for_rent_exemption(data.len()) + }) + token_account_data.amount() + }); + (data, lamports) }; token_account.apply_update(|account| { - // If this is a native mint, we need to adjust the lamports to match the wrapped SOL amount + rent - account.lamports = final_lamports; + // Native balances encode wrapped SOL in lamports, while confidential layouts may resize. + if let Some(final_lamports) = final_lamports { + account.lamports = final_lamports; + } account.data = final_account_bytes.clone(); Ok(()) })?; @@ -5097,6 +5114,209 @@ mod tests { ); } + #[tokio::test(flavor = "multi_thread")] + async fn test_set_token_2022_amount_preserves_extension_account_lamports() { + use spl_token_2022_interface::{ + extension::{ + BaseStateWithExtensions, BaseStateWithExtensionsMut, ExtensionType, + StateWithExtensions, StateWithExtensionsMut, immutable_owner::ImmutableOwner, + }, + state::{Account as Token2022Account, AccountState}, + }; + + let client = TestSetup::new(SurfnetCheatcodesRpc::empty()); + let owner = Keypair::new(); + let mint = Keypair::new(); + let token_program = spl_token_2022_interface::id(); + let associated_token_account = get_associated_token_address_with_program_id( + &owner.pubkey(), + &mint.pubkey(), + &token_program, + ); + + let account_len = ExtensionType::try_calculate_account_len::(&[ + ExtensionType::ImmutableOwner, + ]) + .unwrap(); + let mut data = vec![0; account_len]; + { + let mut state = + StateWithExtensionsMut::::unpack_uninitialized(&mut data) + .unwrap(); + state.base = Token2022Account { + mint: mint.pubkey(), + owner: owner.pubkey(), + amount: 10, + delegate: COption::None, + state: AccountState::Initialized, + is_native: COption::None, + delegated_amount: 0, + close_authority: COption::None, + }; + state.pack_base(); + state.init_account_type().unwrap(); + state.init_extension::(true).unwrap(); + } + + let original_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .minimum_balance_for_rent_exemption(data.len()) + + 123 + }); + set_account( + &client, + &mint.pubkey(), + &Account { + lamports: 1, + data: vec![], + owner: token_program, + executable: false, + rent_epoch: 0, + }, + ); + set_account( + &client, + &associated_token_account, + &Account { + lamports: original_lamports, + data, + owner: token_program, + executable: false, + rent_epoch: 0, + }, + ); + + client + .rpc + .set_token_account( + Some(client.context.clone()), + owner.pubkey().to_string(), + mint.pubkey().to_string(), + TokenAccountUpdate { + amount: Some(42), + ..Default::default() + }, + Some(token_program.to_string()), + ) + .await + .unwrap(); + + let updated = client.context.svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .get_account(&associated_token_account) + .unwrap() + .unwrap() + }); + let state = StateWithExtensions::::unpack(&updated.data).unwrap(); + + assert_eq!(updated.lamports, original_lamports); + assert_eq!(state.base.amount, 42); + assert!(state.get_extension::().is_ok()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_set_native_token_2022_amount_keeps_extended_account_rent_exempt() { + use spl_token_2022_interface::{ + extension::{ + BaseStateWithExtensions, BaseStateWithExtensionsMut, ExtensionType, + StateWithExtensions, StateWithExtensionsMut, immutable_owner::ImmutableOwner, + }, + state::{Account as Token2022Account, AccountState}, + }; + + let client = TestSetup::new(SurfnetCheatcodesRpc::empty()); + let owner = Keypair::new(); + let mint = spl_token_interface::native_mint::id(); + let token_program = spl_token_2022_interface::id(); + let associated_token_account = + get_associated_token_address_with_program_id(&owner.pubkey(), &mint, &token_program); + + let account_len = ExtensionType::try_calculate_account_len::(&[ + ExtensionType::ImmutableOwner, + ]) + .unwrap(); + let extended_rent = client.context.svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .minimum_balance_for_rent_exemption(account_len) + }); + let mut data = vec![0; account_len]; + { + let mut state = + StateWithExtensionsMut::::unpack_uninitialized(&mut data) + .unwrap(); + state.base = Token2022Account { + mint, + owner: owner.pubkey(), + amount: 10, + delegate: COption::None, + state: AccountState::Initialized, + is_native: COption::Some(extended_rent), + delegated_amount: 0, + close_authority: COption::None, + }; + state.pack_base(); + state.init_account_type().unwrap(); + state.init_extension::(true).unwrap(); + } + + set_account( + &client, + &mint, + &Account { + lamports: 1, + data: vec![], + owner: token_program, + executable: false, + rent_epoch: 0, + }, + ); + set_account( + &client, + &associated_token_account, + &Account { + lamports: extended_rent + 10, + data, + owner: token_program, + executable: false, + rent_epoch: 0, + }, + ); + + client + .rpc + .set_token_account( + Some(client.context.clone()), + owner.pubkey().to_string(), + mint.to_string(), + TokenAccountUpdate { + amount: Some(42), + ..Default::default() + }, + Some(token_program.to_string()), + ) + .await + .unwrap(); + + let updated = client.context.svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .get_account(&associated_token_account) + .unwrap() + .unwrap() + }); + let state = StateWithExtensions::::unpack(&updated.data).unwrap(); + + // The wrapped-SOL lamports floor tracks the extended layout, not the + // 165-byte base. + assert_eq!(updated.lamports, extended_rent + 42); + assert_eq!(state.base.amount, 42); + assert_eq!(state.base.is_native, COption::Some(extended_rent)); + assert!(state.get_extension::().is_ok()); + } + #[tokio::test(flavor = "multi_thread")] async fn test_set_confidential_token_account_spendable() { use bytemuck::bytes_of; diff --git a/crates/core/src/scenarios/README.md b/crates/core/src/scenarios/README.md index 4368f2b85..10950e578 100644 --- a/crates/core/src/scenarios/README.md +++ b/crates/core/src/scenarios/README.md @@ -18,6 +18,8 @@ Protocols that are natively supported by Surfpool will have their IDLs included - **Switchboard On-Demand** - On-demand oracle with QuoteAccount override template - **Kamino v1.x** – Lending protocol with Reserve liquidity, risk config, and Obligation health override templates - **Drift v2** - Perp and spot markets, user state, and global state +- **Pump v1** - Bonding curve launchpad with curve reserve and global config override templates +- **PumpSwap v1** - Constant-product AMM with pool state and global config override templates, including canonical pool derivation for migrated pump.fun coins For custom protocols, an IDL can be registered at runtime using the [`surfnet_registerIdl`](https://docs.surfpool.run/rpc/cheatcodes#surfnet-registeridl) RPC cheatcode. diff --git a/crates/core/src/scenarios/mod.rs b/crates/core/src/scenarios/mod.rs index b258bb5b7..a537df539 100644 --- a/crates/core/src/scenarios/mod.rs +++ b/crates/core/src/scenarios/mod.rs @@ -1,3 +1,4 @@ +pub mod protocols; pub mod registry; pub use registry::TemplateRegistry; diff --git a/crates/core/src/scenarios/protocols/mod.rs b/crates/core/src/scenarios/protocols/mod.rs new file mode 100644 index 000000000..99f0b0967 --- /dev/null +++ b/crates/core/src/scenarios/protocols/mod.rs @@ -0,0 +1 @@ +pub mod pump; diff --git a/crates/core/src/scenarios/protocols/pump-amm/README.md b/crates/core/src/scenarios/protocols/pump-amm/README.md new file mode 100644 index 000000000..adde57327 --- /dev/null +++ b/crates/core/src/scenarios/protocols/pump-amm/README.md @@ -0,0 +1,65 @@ +# PumpSwap (pump-amm) + +The AMM a pump.fun coin trades on after its bonding curve completes and migrates. For the +bonding-curve side and the full lifecycle, see [`../pump/README.md`](../pump/README.md). + +Program: `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA`. The IDL is copied verbatim from +pump-public-docs (`idl/pump_amm.json`). + +## Templates + +| Template | Account | Selected by | Use for | +| ------------------------- | -------------- | ------------- | --------------------------------------------------------------------------------- | +| `pump-amm-pool-state` | `Pool` | pool address | any pool, including non-canonical or non-WSOL ones | +| `pump-amm-canonical-pool` | `Pool` | coin mint | the canonical WSOL pool of a migrated coin, derived so you don't need its address | +| `pump-amm-global-config` | `GlobalConfig` | — (singleton) | pool fees and disable flags | + +## Field reference + +What each overridable field means and what overriding it lets you model. + +### `Pool` + +| Field | Meaning | Override it to | +| ------------------------ | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `lp_supply` | Total LP token supply before user burns and lock-ups | model LP state | +| `coin_creator` | Pubkey accruing the coin-creator fee for this pool | point creator fees at a key you control | +| `virtual_quote_reserves` | Appended quote reserves added to the quote vault when quoting (0 on every pool today) | shift the effective quote (reprice) without touching any vault balance | + +The price-setting reserves live in the pool's token accounts (`pool_base_token_account` / +`pool_quote_token_account`), not the `Pool` account - move those with the spl-token template. + +### `GlobalConfig` (singleton, `["global_config"]`) + +| Field | Meaning | Override it to | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------- | +| `lp_fee_basis_points`, `protocol_fee_basis_points`, `coin_creator_fee_basis_points` | Legacy flat fees; live trades read the fee program's `FeeConfig` | legacy - won't change what a swap charges | +| `disable_flags` | Bitmask disabling individual instructions (0 = everything enabled) | disable specific instructions to test error paths | + +## Pricing + +PumpSwap is a constant-product AMM. The reserves that set the price live in the pool's two +token accounts, not in the `Pool` account. Effective quote reserves are the quote vault +balance plus `Pool.virtual_quote_reserves` (which is 0 on every pool today). + +Two ways to move the price: + +- override `virtual_quote_reserves` on the pool — shifts the effective quote without + touching any balance; +- override the vault balances with the spl-token template — the vault addresses are in the + `Pool` account's `pool_base_token_account` / `pool_quote_token_account` fields. + +For the canonical WSOL pool of a migrated coin, the Studio preset loads the existing +`pump-amm-canonical-pool` template and stores a standard scenario. AI clients use the same +template through the generic `create_scenario` MCP tool. See the complete example in +[`../pump/README.md`](../pump/README.md). + +## Notes + +- The canonical template only works for coins that migrated to PumpSwap (roughly March 2025 + onward). Coins that graduated earlier went to Raydium and have no canonical pool — use + `pump-amm-pool-state` with the pool address for those. +- Fees on a live trade come from the external fee program's `FeeConfig`, not from the + basis-point fields on `GlobalConfig` (those are legacy). Overriding them here won't change + what a swap charges. +- Always set `fetchBeforeUse: true` so the fields you don't override keep their live values. diff --git a/crates/core/src/scenarios/protocols/pump-amm/v1/idl.json b/crates/core/src/scenarios/protocols/pump-amm/v1/idl.json new file mode 100644 index 000000000..a654b6f92 --- /dev/null +++ b/crates/core/src/scenarios/protocols/pump-amm/v1/idl.json @@ -0,0 +1,7390 @@ +{ + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", + "metadata": { + "name": "pump_amm", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "admin_set_coin_creator", + "docs": [ + "Overrides the coin creator for a canonical pump pool" + ], + "discriminator": [ + 242, + 40, + 117, + 145, + 73, + 96, + 105, + 104 + ], + "accounts": [ + { + "name": "admin_set_coin_creator_authority", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "coin_creator", + "type": "pubkey" + } + ] + }, + { + "name": "admin_update_token_incentives", + "discriminator": [ + 209, + 11, + 115, + 87, + 213, + 23, + 124, + 204 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "global_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + } + ] + } + }, + { + "name": "mint" + }, + { + "name": "global_incentive_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "global_volume_accumulator" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "start_time", + "type": "i64" + }, + { + "name": "end_time", + "type": "i64" + }, + { + "name": "seconds_in_a_day", + "type": "i64" + }, + { + "name": "day_number", + "type": "u64" + }, + { + "name": "token_supply_per_day", + "type": "u64" + } + ] + }, + { + "name": "boost_buy_and_burn", + "discriminator": [ + 105, + 68, + 6, + 175, + 0, + 7, + 35, + 162 + ], + "accounts": [ + { + "name": "pool" + }, + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "global_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "base_mint", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "quote_mint", + "relations": [ + "pool" + ] + }, + { + "name": "pool_base_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "pool_quote_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "boost_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 111, + 115, + 116, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "boost_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "boost_vault_authority" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "quote_amount_in", + "type": "u64" + }, + { + "name": "min_base_amount_burned", + "type": "u64" + } + ] + }, + { + "name": "buy", + "docs": [ + "For cashback coins, optionally pass user_volume_accumulator_wsol_ata as remaining_accounts[0].", + "If provided and valid, the ATA will be initialized if needed." + ], + "discriminator": [ + 102, + 6, + 61, + 18, + 1, + 218, + 235, + 234 + ], + "accounts": [ + { + "name": "pool", + "writable": true + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "global_config" + }, + { + "name": "base_mint", + "relations": [ + "pool" + ] + }, + { + "name": "quote_mint", + "relations": [ + "pool" + ] + }, + { + "name": "user_base_token_account", + "writable": true + }, + { + "name": "user_quote_token_account", + "writable": true + }, + { + "name": "pool_base_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "pool_quote_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "protocol_fee_recipient" + }, + { + "name": "protocol_fee_recipient_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "protocol_fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "coin_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "coin_creator_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool.coin_creator", + "account": "Pool" + } + ] + } + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "const", + "value": [ + 12, + 20, + 222, + 252, + 130, + 94, + 198, + 118, + 148, + 37, + 8, + 24, + 187, + 101, + 64, + 101, + 244, + 41, + 141, + 49, + 86, + 213, + 113, + 180, + 212, + 248, + 9, + 12, + 24, + 233, + 168, + 99 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + } + ], + "args": [ + { + "name": "base_amount_out", + "type": "u64" + }, + { + "name": "max_quote_amount_in", + "type": "u64" + }, + { + "name": "track_volume", + "type": { + "defined": { + "name": "OptionBool" + } + } + } + ] + }, + { + "name": "buy_exact_quote_in", + "docs": [ + "Given a budget of spendable_quote_in, buy at least min_base_amount_out", + "Fees will be deducted from spendable_quote_in", + "", + "f(quote) = tokens, where tokens >= min_base_amount_out", + "", + "Make sure the payer has enough SOL to cover creation of the following accounts (unless already created):", + "- protocol_fee_recipient_token_account: rent.minimum_balance(TokenAccount::LEN)", + "- coin_creator_vault_ata: rent.minimum_balance(TokenAccount::LEN)", + "- user_volume_accumulator: rent.minimum_balance(UserVolumeAccumulator::LEN)", + "", + "For cashback coins, optionally pass user_volume_accumulator_wsol_ata as remaining_accounts[0].", + "If provided and valid, the ATA will be initialized if needed." + ], + "discriminator": [ + 198, + 46, + 21, + 82, + 180, + 217, + 232, + 112 + ], + "accounts": [ + { + "name": "pool", + "writable": true + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "global_config" + }, + { + "name": "base_mint", + "relations": [ + "pool" + ] + }, + { + "name": "quote_mint", + "relations": [ + "pool" + ] + }, + { + "name": "user_base_token_account", + "writable": true + }, + { + "name": "user_quote_token_account", + "writable": true + }, + { + "name": "pool_base_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "pool_quote_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "protocol_fee_recipient" + }, + { + "name": "protocol_fee_recipient_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "protocol_fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "coin_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "coin_creator_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool.coin_creator", + "account": "Pool" + } + ] + } + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "const", + "value": [ + 12, + 20, + 222, + 252, + 130, + 94, + 198, + 118, + 148, + 37, + 8, + 24, + 187, + 101, + 64, + 101, + 244, + 41, + 141, + 49, + 86, + 213, + 113, + 180, + 212, + 248, + 9, + 12, + 24, + 233, + 168, + 99 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + } + ], + "args": [ + { + "name": "spendable_quote_in", + "type": "u64" + }, + { + "name": "min_base_amount_out", + "type": "u64" + }, + { + "name": "track_volume", + "type": { + "defined": { + "name": "OptionBool" + } + } + } + ] + }, + { + "name": "claim_cashback", + "discriminator": [ + 37, + 58, + 35, + 126, + 190, + 53, + 228, + 197 + ], + "accounts": [ + { + "name": "user", + "writable": true + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "quote_mint" + }, + { + "name": "quote_token_program" + }, + { + "name": "user_volume_accumulator_wsol_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "user_wsol_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + } + ], + "args": [] + }, + { + "name": "claim_token_incentives", + "discriminator": [ + 16, + 4, + 71, + 28, + 204, + 1, + 40, + 27 + ], + "accounts": [ + { + "name": "user" + }, + { + "name": "user_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + } + ] + } + }, + { + "name": "global_incentive_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "global_volume_accumulator" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "mint", + "relations": [ + "global_volume_accumulator" + ] + }, + { + "name": "token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "payer", + "writable": true, + "signer": true + } + ], + "args": [] + }, + { + "name": "close_user_volume_accumulator", + "discriminator": [ + 249, + 69, + 164, + 218, + 150, + 103, + 84, + 138 + ], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "collect_coin_creator_fee", + "discriminator": [ + 160, + 57, + 89, + 42, + 181, + 139, + 43, + 66 + ], + "accounts": [ + { + "name": "quote_mint" + }, + { + "name": "quote_token_program" + }, + { + "name": "coin_creator" + }, + { + "name": "coin_creator_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "coin_creator" + } + ] + } + }, + { + "name": "coin_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "coin_creator_token_account", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "create_config", + "discriminator": [ + 201, + 207, + 243, + 114, + 75, + 111, + 47, + 189 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true, + "address": "8LWu7QM2dGR1G8nKDHthckea57bkCzXyBTAKPJUBDHo8" + }, + { + "name": "global_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "lp_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee_recipients", + "type": { + "array": [ + "pubkey", + 8 + ] + } + }, + { + "name": "coin_creator_fee_basis_points", + "type": "u64" + }, + { + "name": "admin_set_coin_creator_authority", + "type": "pubkey" + } + ] + }, + { + "name": "create_pool", + "discriminator": [ + 233, + 146, + 209, + 142, + 207, + 104, + 64, + 188 + ], + "accounts": [ + { + "name": "pool", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "arg", + "path": "index" + }, + { + "kind": "account", + "path": "creator" + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "global_config" + }, + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "lp_mint", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 108, + 112, + 95, + 109, + 105, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "user_base_token_account", + "writable": true + }, + { + "name": "user_quote_token_account", + "writable": true + }, + { + "name": "user_pool_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator" + }, + { + "kind": "account", + "path": "token_2022_program" + }, + { + "kind": "account", + "path": "lp_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "pool_base_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "pool_quote_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_2022_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "index", + "type": "u16" + }, + { + "name": "base_amount_in", + "type": "u64" + }, + { + "name": "quote_amount_in", + "type": "u64" + }, + { + "name": "coin_creator", + "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" + }, + { + "name": "is_cashback_coin", + "type": { + "defined": { + "name": "OptionBool" + } + } + } + ] + }, + { + "name": "deposit", + "discriminator": [ + 242, + 35, + 198, + 137, + 82, + 225, + 242, + 182 + ], + "accounts": [ + { + "name": "pool", + "writable": true + }, + { + "name": "global_config" + }, + { + "name": "user", + "signer": true + }, + { + "name": "base_mint", + "relations": [ + "pool" + ] + }, + { + "name": "quote_mint", + "relations": [ + "pool" + ] + }, + { + "name": "lp_mint", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "user_base_token_account", + "writable": true + }, + { + "name": "user_quote_token_account", + "writable": true + }, + { + "name": "user_pool_token_account", + "writable": true + }, + { + "name": "pool_base_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "pool_quote_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "token_2022_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "lp_token_amount_out", + "type": "u64" + }, + { + "name": "max_base_amount_in", + "type": "u64" + }, + { + "name": "max_quote_amount_in", + "type": "u64" + } + ] + }, + { + "name": "disable", + "discriminator": [ + 185, + 173, + 187, + 90, + 216, + 15, + 238, + 233 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "disable_create_pool", + "type": "bool" + }, + { + "name": "disable_deposit", + "type": "bool" + }, + { + "name": "disable_withdraw", + "type": "bool" + }, + { + "name": "disable_buy", + "type": "bool" + }, + { + "name": "disable_sell", + "type": "bool" + } + ] + }, + { + "name": "extend_account", + "discriminator": [ + 234, + 102, + 194, + 203, + 150, + 72, + 62, + 229 + ], + "accounts": [ + { + "name": "account", + "writable": true + }, + { + "name": "user", + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "init_boost", + "discriminator": [ + 140, + 233, + 33, + 94, + 132, + 90, + 194, + 143 + ], + "accounts": [ + { + "name": "pool", + "writable": true + }, + { + "name": "global_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "base_mint", + "relations": [ + "pool" + ] + }, + { + "name": "quote_mint", + "relations": [ + "pool" + ] + }, + { + "name": "pool_base_token_account", + "relations": [ + "pool" + ] + }, + { + "name": "pool_quote_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "boost_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 111, + 115, + 116, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "boost_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "boost_vault_authority" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "quote_token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "init_user_volume_accumulator", + "discriminator": [ + 94, + 6, + 202, + 115, + 255, + 96, + 232, + 183 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "user" + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "migrate_pool_coin_creator", + "docs": [ + "Migrate Pool Coin Creator to Sharing Config" + ], + "discriminator": [ + 208, + 8, + 159, + 4, + 74, + 175, + 16, + 58 + ], + "accounts": [ + { + "name": "pool", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "account", + "path": "pool.index", + "account": "Pool" + }, + { + "kind": "account", + "path": "pool.creator", + "account": "Pool" + }, + { + "kind": "account", + "path": "pool.base_mint", + "account": "Pool" + }, + { + "kind": "account", + "path": "pool.quote_mint", + "account": "Pool" + } + ] + } + }, + { + "name": "sharing_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 104, + 97, + 114, + 105, + 110, + 103, + 45, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "pool.base_mint", + "account": "Pool" + } + ], + "program": { + "kind": "const", + "value": [ + 12, + 53, + 255, + 169, + 5, + 90, + 142, + 86, + 141, + 168, + 247, + 188, + 7, + 86, + 21, + 39, + 76, + 241, + 201, + 44, + 164, + 31, + 64, + 0, + 156, + 81, + 106, + 164, + 20, + 194, + 124, + 112 + ] + } + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "sell", + "discriminator": [ + 51, + 230, + 133, + 164, + 1, + 127, + 131, + 173 + ], + "accounts": [ + { + "name": "pool", + "writable": true + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "global_config" + }, + { + "name": "base_mint", + "relations": [ + "pool" + ] + }, + { + "name": "quote_mint", + "relations": [ + "pool" + ] + }, + { + "name": "user_base_token_account", + "writable": true + }, + { + "name": "user_quote_token_account", + "writable": true + }, + { + "name": "pool_base_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "pool_quote_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "protocol_fee_recipient" + }, + { + "name": "protocol_fee_recipient_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "protocol_fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "coin_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "coin_creator_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool.coin_creator", + "account": "Pool" + } + ] + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "const", + "value": [ + 12, + 20, + 222, + 252, + 130, + 94, + 198, + 118, + 148, + 37, + 8, + 24, + 187, + 101, + 64, + 101, + 244, + 41, + 141, + 49, + 86, + 213, + 113, + 180, + 212, + 248, + 9, + 12, + 24, + 233, + 168, + 99 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + } + ], + "args": [ + { + "name": "base_amount_in", + "type": "u64" + }, + { + "name": "min_quote_amount_out", + "type": "u64" + } + ] + }, + { + "name": "set_boost_authority", + "discriminator": [ + 227, + 149, + 76, + 42, + 130, + 39, + 234, + 205 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "name": "boost_authority" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "set_coin_creator", + "docs": [ + "Sets Pool::coin_creator from Metaplex metadata creator or BondingCurve::creator" + ], + "discriminator": [ + 210, + 149, + 128, + 45, + 188, + 58, + 78, + 175 + ], + "accounts": [ + { + "name": "pool", + "writable": true + }, + { + "name": "metadata", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97, + 100, + 97, + 116, + 97 + ] + }, + { + "kind": "const", + "value": [ + 11, + 112, + 101, + 177, + 227, + 209, + 124, + 69, + 56, + 157, + 82, + 127, + 107, + 4, + 195, + 205, + 88, + 184, + 108, + 115, + 26, + 160, + 253, + 181, + 73, + 182, + 209, + 188, + 3, + 248, + 41, + 70 + ] + }, + { + "kind": "account", + "path": "pool.base_mint", + "account": "Pool" + } + ], + "program": { + "kind": "const", + "value": [ + 11, + 112, + 101, + 177, + 227, + 209, + 124, + 69, + 56, + 157, + 82, + 127, + 107, + 4, + 195, + 205, + 88, + 184, + 108, + 115, + 26, + 160, + 253, + 181, + 73, + 182, + 209, + 188, + 3, + 248, + 41, + 70 + ] + } + } + }, + { + "name": "bonding_curve", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 110, + 100, + 105, + 110, + 103, + 45, + 99, + 117, + 114, + 118, + 101 + ] + }, + { + "kind": "account", + "path": "pool.base_mint", + "account": "Pool" + } + ], + "program": { + "kind": "const", + "value": [ + 1, + 86, + 224, + 246, + 147, + 102, + 90, + 207, + 68, + 219, + 21, + 104, + 191, + 23, + 91, + 170, + 81, + 137, + 203, + 151, + 245, + 210, + 255, + 59, + 101, + 93, + 43, + 182, + 253, + 109, + 24, + 176 + ] + } + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "set_reserved_fee_recipients", + "discriminator": [ + 111, + 172, + 162, + 232, + 114, + 89, + 213, + 142 + ], + "accounts": [ + { + "name": "global_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "whitelist_pda", + "type": "pubkey" + } + ] + }, + { + "name": "sync_user_volume_accumulator", + "discriminator": [ + 86, + 31, + 192, + 87, + 163, + 87, + 79, + 238 + ], + "accounts": [ + { + "name": "user" + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "toggle_boost", + "discriminator": [ + 117, + 161, + 160, + 74, + 223, + 137, + 118, + 99 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + } + ], + "args": [ + { + "name": "enabled", + "type": "bool" + } + ] + }, + { + "name": "toggle_cashback_enabled", + "discriminator": [ + 115, + 103, + 224, + 255, + 189, + 89, + 86, + 195 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "enabled", + "type": "bool" + } + ] + }, + { + "name": "toggle_mayhem_mode", + "discriminator": [ + 1, + 9, + 111, + 208, + 100, + 31, + 255, + 163 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "enabled", + "type": "bool" + } + ] + }, + { + "name": "transfer_creator_fees_to_pump", + "docs": [ + "Transfer creator fees to pump creator vault", + "If coin creator fees are currently below rent.minimum_balance(TokenAccount::LEN)", + "The transfer will be skipped" + ], + "discriminator": [ + 139, + 52, + 134, + 85, + 228, + 229, + 108, + 241 + ], + "accounts": [ + { + "name": "wsol_mint", + "docs": [ + "Pump Canonical Pool are quoted in wSOL" + ] + }, + { + "name": "token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "coin_creator" + }, + { + "name": "coin_creator_vault_authority", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "coin_creator" + } + ] + } + }, + { + "name": "coin_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "wsol_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "pump_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 45, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "coin_creator" + } + ], + "program": { + "kind": "const", + "value": [ + 1, + 86, + 224, + 246, + 147, + 102, + 90, + 207, + 68, + 219, + 21, + 104, + 191, + 23, + 91, + 170, + 81, + 137, + 203, + 151, + 245, + 210, + 255, + 59, + 101, + 93, + 43, + 182, + 253, + 109, + 24, + 176 + ] + } + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "transfer_creator_fees_to_pump_v2", + "discriminator": [ + 1, + 33, + 78, + 185, + 33, + 67, + 44, + 92 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "quote_mint" + }, + { + "name": "token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "coin_creator" + }, + { + "name": "coin_creator_vault_authority", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "coin_creator" + } + ] + } + }, + { + "name": "coin_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "coin_creator_vault_authority" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "pump_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 45, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "coin_creator" + } + ], + "program": { + "kind": "const", + "value": [ + 1, + 86, + 224, + 246, + 147, + 102, + 90, + 207, + 68, + 219, + 21, + 104, + 191, + 23, + 91, + 170, + 81, + 137, + 203, + 151, + 245, + 210, + 255, + 59, + 101, + 93, + 43, + 182, + 253, + 109, + 24, + 176 + ] + } + } + }, + { + "name": "pump_creator_vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pump_creator_vault" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "update_admin", + "discriminator": [ + 161, + 176, + 40, + 213, + 60, + 184, + 179, + 228 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "name": "new_admin" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "update_buyback_config", + "discriminator": [ + 251, + 224, + 171, + 146, + 160, + 26, + 113, + 233 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "buyback_basis_points", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "update_fee_config", + "discriminator": [ + 104, + 184, + 103, + 242, + 88, + 151, + 107, + 20 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "lp_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee_recipients", + "type": { + "array": [ + "pubkey", + 8 + ] + } + }, + { + "name": "coin_creator_fee_basis_points", + "type": "u64" + }, + { + "name": "admin_set_coin_creator_authority", + "type": "pubkey" + } + ] + }, + { + "name": "withdraw", + "discriminator": [ + 183, + 18, + 70, + 156, + 148, + 109, + 161, + 34 + ], + "accounts": [ + { + "name": "pool", + "writable": true + }, + { + "name": "global_config" + }, + { + "name": "user", + "signer": true + }, + { + "name": "base_mint", + "relations": [ + "pool" + ] + }, + { + "name": "quote_mint", + "relations": [ + "pool" + ] + }, + { + "name": "lp_mint", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "user_base_token_account", + "writable": true + }, + { + "name": "user_quote_token_account", + "writable": true + }, + { + "name": "user_pool_token_account", + "writable": true + }, + { + "name": "pool_base_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "pool_quote_token_account", + "writable": true, + "relations": [ + "pool" + ] + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "token_2022_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "lp_token_amount_in", + "type": "u64" + }, + { + "name": "min_base_amount_out", + "type": "u64" + }, + { + "name": "min_quote_amount_out", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "BondingCurve", + "discriminator": [ + 23, + 183, + 248, + 55, + 96, + 216, + 172, + 96 + ] + }, + { + "name": "FeeConfig", + "discriminator": [ + 143, + 52, + 146, + 187, + 219, + 123, + 76, + 155 + ] + }, + { + "name": "GlobalConfig", + "discriminator": [ + 149, + 8, + 156, + 202, + 160, + 252, + 176, + 217 + ] + }, + { + "name": "GlobalVolumeAccumulator", + "discriminator": [ + 202, + 42, + 246, + 43, + 142, + 190, + 30, + 255 + ] + }, + { + "name": "Pool", + "discriminator": [ + 241, + 154, + 109, + 4, + 17, + 177, + 109, + 188 + ] + }, + { + "name": "SharingConfig", + "discriminator": [ + 216, + 74, + 9, + 0, + 56, + 140, + 93, + 75 + ] + }, + { + "name": "UserVolumeAccumulator", + "discriminator": [ + 86, + 255, + 112, + 14, + 102, + 53, + 154, + 250 + ] + } + ], + "events": [ + { + "name": "AdminSetCoinCreatorEvent", + "discriminator": [ + 45, + 220, + 93, + 24, + 25, + 97, + 172, + 104 + ] + }, + { + "name": "AdminUpdateTokenIncentivesEvent", + "discriminator": [ + 147, + 250, + 108, + 120, + 247, + 29, + 67, + 222 + ] + }, + { + "name": "BoostBuyAndBurnEvent", + "discriminator": [ + 63, + 69, + 28, + 22, + 48, + 92, + 194, + 185 + ] + }, + { + "name": "BuyEvent", + "discriminator": [ + 103, + 244, + 82, + 31, + 44, + 245, + 119, + 119 + ] + }, + { + "name": "ClaimCashbackEvent", + "discriminator": [ + 226, + 214, + 246, + 33, + 7, + 242, + 147, + 229 + ] + }, + { + "name": "ClaimTokenIncentivesEvent", + "discriminator": [ + 79, + 172, + 246, + 49, + 205, + 91, + 206, + 232 + ] + }, + { + "name": "CloseUserVolumeAccumulatorEvent", + "discriminator": [ + 146, + 159, + 189, + 172, + 146, + 88, + 56, + 244 + ] + }, + { + "name": "CollectCoinCreatorFeeEvent", + "discriminator": [ + 232, + 245, + 194, + 238, + 234, + 218, + 58, + 89 + ] + }, + { + "name": "CreateConfigEvent", + "discriminator": [ + 107, + 52, + 89, + 129, + 55, + 226, + 81, + 22 + ] + }, + { + "name": "CreatePoolEvent", + "discriminator": [ + 177, + 49, + 12, + 210, + 160, + 118, + 167, + 116 + ] + }, + { + "name": "DepositEvent", + "discriminator": [ + 120, + 248, + 61, + 83, + 31, + 142, + 107, + 144 + ] + }, + { + "name": "DisableEvent", + "discriminator": [ + 107, + 253, + 193, + 76, + 228, + 202, + 27, + 104 + ] + }, + { + "name": "ExtendAccountEvent", + "discriminator": [ + 97, + 97, + 215, + 144, + 93, + 146, + 22, + 124 + ] + }, + { + "name": "InitBoostEvent", + "discriminator": [ + 174, + 124, + 74, + 249, + 4, + 81, + 246, + 17 + ] + }, + { + "name": "InitUserVolumeAccumulatorEvent", + "discriminator": [ + 134, + 36, + 13, + 72, + 232, + 101, + 130, + 216 + ] + }, + { + "name": "MigratePoolCoinCreatorEvent", + "discriminator": [ + 170, + 221, + 82, + 199, + 147, + 165, + 247, + 46 + ] + }, + { + "name": "ReservedFeeRecipientsEvent", + "discriminator": [ + 43, + 188, + 250, + 18, + 221, + 75, + 187, + 95 + ] + }, + { + "name": "SellEvent", + "discriminator": [ + 62, + 47, + 55, + 10, + 165, + 3, + 220, + 42 + ] + }, + { + "name": "SetBondingCurveCoinCreatorEvent", + "discriminator": [ + 242, + 231, + 235, + 102, + 65, + 99, + 189, + 211 + ] + }, + { + "name": "SetBoostAuthorityEvent", + "discriminator": [ + 89, + 128, + 240, + 141, + 91, + 202, + 71, + 105 + ] + }, + { + "name": "SetMetaplexCoinCreatorEvent", + "discriminator": [ + 150, + 107, + 199, + 123, + 124, + 207, + 102, + 228 + ] + }, + { + "name": "SyncUserVolumeAccumulatorEvent", + "discriminator": [ + 197, + 122, + 167, + 124, + 116, + 81, + 91, + 255 + ] + }, + { + "name": "UpdateAdminEvent", + "discriminator": [ + 225, + 152, + 171, + 87, + 246, + 63, + 66, + 234 + ] + }, + { + "name": "UpdateFeeConfigEvent", + "discriminator": [ + 90, + 23, + 65, + 35, + 62, + 244, + 188, + 208 + ] + }, + { + "name": "WithdrawEvent", + "discriminator": [ + 22, + 9, + 133, + 26, + 160, + 44, + 71, + 192 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "FeeBasisPointsExceedsMaximum" + }, + { + "code": 6001, + "name": "ZeroBaseAmount" + }, + { + "code": 6002, + "name": "ZeroQuoteAmount" + }, + { + "code": 6003, + "name": "TooLittlePoolTokenLiquidity" + }, + { + "code": 6004, + "name": "ExceededSlippage" + }, + { + "code": 6005, + "name": "InvalidAdmin" + }, + { + "code": 6006, + "name": "UnsupportedBaseMint" + }, + { + "code": 6007, + "name": "UnsupportedQuoteMint" + }, + { + "code": 6008, + "name": "InvalidBaseMint" + }, + { + "code": 6009, + "name": "InvalidQuoteMint" + }, + { + "code": 6010, + "name": "InvalidLpMint" + }, + { + "code": 6011, + "name": "AllProtocolFeeRecipientsShouldBeNonZero" + }, + { + "code": 6012, + "name": "UnsortedNotUniqueProtocolFeeRecipients" + }, + { + "code": 6013, + "name": "InvalidProtocolFeeRecipient" + }, + { + "code": 6014, + "name": "InvalidPoolBaseTokenAccount" + }, + { + "code": 6015, + "name": "InvalidPoolQuoteTokenAccount" + }, + { + "code": 6016, + "name": "BuyMoreBaseAmountThanPoolReserves" + }, + { + "code": 6017, + "name": "DisabledCreatePool" + }, + { + "code": 6018, + "name": "DisabledDeposit" + }, + { + "code": 6019, + "name": "DisabledWithdraw" + }, + { + "code": 6020, + "name": "DisabledBuy" + }, + { + "code": 6021, + "name": "DisabledSell" + }, + { + "code": 6022, + "name": "SameMint" + }, + { + "code": 6023, + "name": "Overflow" + }, + { + "code": 6024, + "name": "Truncation" + }, + { + "code": 6025, + "name": "DivisionByZero" + }, + { + "code": 6026, + "name": "NewSizeLessThanCurrentSize" + }, + { + "code": 6027, + "name": "AccountTypeNotSupported" + }, + { + "code": 6028, + "name": "OnlyCanonicalPumpPoolsCanHaveCoinCreator" + }, + { + "code": 6029, + "name": "InvalidAdminSetCoinCreatorAuthority" + }, + { + "code": 6030, + "name": "StartTimeInThePast" + }, + { + "code": 6031, + "name": "EndTimeInThePast" + }, + { + "code": 6032, + "name": "EndTimeBeforeStartTime" + }, + { + "code": 6033, + "name": "TimeRangeTooLarge" + }, + { + "code": 6034, + "name": "EndTimeBeforeCurrentDay" + }, + { + "code": 6035, + "name": "SupplyUpdateForFinishedRange" + }, + { + "code": 6036, + "name": "DayIndexAfterEndIndex" + }, + { + "code": 6037, + "name": "DayInActiveRange" + }, + { + "code": 6038, + "name": "InvalidIncentiveMint" + }, + { + "code": 6039, + "name": "BuyNotEnoughQuoteTokensToCoverFees", + "msg": "buy: Not enough quote tokens to cover for fees." + }, + { + "code": 6040, + "name": "BuySlippageBelowMinBaseAmountOut", + "msg": "buy: slippage - would buy less tokens than expected min_base_amount_out" + }, + { + "code": 6041, + "name": "MayhemModeDisabled" + }, + { + "code": 6042, + "name": "OnlyPumpPoolsMayhemMode" + }, + { + "code": 6043, + "name": "MayhemModeInDesiredState" + }, + { + "code": 6044, + "name": "NotEnoughRemainingAccounts" + }, + { + "code": 6045, + "name": "InvalidSharingConfigBaseMint" + }, + { + "code": 6046, + "name": "InvalidSharingConfigCoinCreator" + }, + { + "code": 6047, + "name": "CoinCreatorMigratedToSharingConfig", + "msg": "coin creator has been migrated to sharing config, use pump_fees::reset_fee_sharing_config instead" + }, + { + "code": 6048, + "name": "CreatorVaultMigratedToSharingConfig", + "msg": "creator_vault has been migrated to sharing config, use pump:distribute_creator_fees instead" + }, + { + "code": 6049, + "name": "CashbackNotEnabled", + "msg": "Cashback is disabled" + }, + { + "code": 6050, + "name": "OnlyPumpPoolsCashback" + }, + { + "code": 6051, + "name": "CashbackNotInDesiredState" + }, + { + "code": 6052, + "name": "TokensInVaultLessThanCashbackEarned" + }, + { + "code": 6053, + "name": "BuybackFeeRecipientNotAuthorized", + "msg": "Buyback fee recipient not authorized" + }, + { + "code": 6054, + "name": "AllBuybackFeeRecipientsShouldBeNonZero" + }, + { + "code": 6055, + "name": "NotUniqueBuybackFeeRecipients" + }, + { + "code": 6056, + "name": "BuybackBasisPointsOutOfRange", + "msg": "buyback_basis_points must be <= 10_000" + }, + { + "code": 6057, + "name": "WrongBuybackFeeRecipientsCount", + "msg": "buyback fee recipients require exactly 8 remaining accounts (or none)" + }, + { + "code": 6058, + "name": "BuybackFeeRecipientMissing" + }, + { + "code": 6059, + "name": "MissingCashbackAccounts", + "msg": "Cashback trade is missing the required remaining accounts" + }, + { + "code": 6060, + "name": "InvalidCashbackAccumulator", + "msg": "Cashback user_volume_accumulator account is invalid" + }, + { + "code": 6061, + "name": "InvalidCashbackAccumulatorAta", + "msg": "Cashback user_volume_accumulator ATA is missing or invalid" + }, + { + "code": 6062, + "name": "InvalidPoolV2", + "msg": "pool_v2 remaining account is missing or invalid" + }, + { + "code": 6063, + "name": "InsufficientRealQuoteReserves", + "msg": "BOOST: sell output exceeds the real quote vault. effective = real + virtual is pricing-only; payout is capped at real_vault, so quote min(out, real_vault)" + }, + { + "code": 6064, + "name": "BoostPoolLiquidityUnsupported", + "msg": "BOOST: deposit/withdraw don't apply to boost pools" + }, + { + "code": 6065, + "name": "PoolCannotBoost", + "msg": "BOOST: pool cannot be boosted (no virtual reserves)" + }, + { + "code": 6066, + "name": "BoostDisabled", + "msg": "BOOST: boost is disabled" + }, + { + "code": 6067, + "name": "SeedLockViolation", + "msg": "BOOST: lp_supply must never drop below the circulating LP mint supply" + } + ], + "types": [ + { + "name": "AdminSetCoinCreatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin_set_coin_creator_authority", + "type": "pubkey" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "old_coin_creator", + "type": "pubkey" + }, + { + "name": "new_coin_creator", + "type": "pubkey" + } + ] + } + }, + { + "name": "AdminUpdateTokenIncentivesEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "start_time", + "type": "i64" + }, + { + "name": "end_time", + "type": "i64" + }, + { + "name": "day_number", + "type": "u64" + }, + { + "name": "token_supply_per_day", + "type": "u64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "seconds_in_a_day", + "type": "i64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "BondingCurve", + "type": { + "kind": "struct", + "fields": [ + { + "name": "virtual_token_reserves", + "type": "u64" + }, + { + "name": "virtual_sol_reserves", + "type": "u64" + }, + { + "name": "real_token_reserves", + "type": "u64" + }, + { + "name": "real_sol_reserves", + "type": "u64" + }, + { + "name": "token_total_supply", + "type": "u64" + }, + { + "name": "complete", + "type": "bool" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" + }, + { + "name": "is_cashback_coin", + "type": "bool" + } + ] + } + }, + { + "name": "BoostBuyAndBurnEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "quote_amount_in_requested", + "type": "u64" + }, + { + "name": "quote_amount_in_used", + "type": "u64" + }, + { + "name": "base_amount_burned", + "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "real_quote_reserves_after", + "type": "u64" + }, + { + "name": "base_reserves_after", + "type": "u64" + }, + { + "name": "boost_vault_remaining", + "type": "u64" + } + ] + } + }, + { + "name": "BuyEvent", + "docs": [ + "ix_name: \"buy\" | \"buy_exact_quote_in\"" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "base_amount_out", + "type": "u64" + }, + { + "name": "max_quote_amount_in", + "type": "u64" + }, + { + "name": "user_base_token_reserves", + "type": "u64" + }, + { + "name": "user_quote_token_reserves", + "type": "u64" + }, + { + "name": "pool_base_token_reserves", + "type": "u64" + }, + { + "name": "pool_quote_token_reserves", + "type": "u64" + }, + { + "name": "quote_amount_in", + "type": "u64" + }, + { + "name": "lp_fee_basis_points", + "type": "u64" + }, + { + "name": "lp_fee", + "type": "u64" + }, + { + "name": "protocol_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee", + "type": "u64" + }, + { + "name": "quote_amount_in_with_lp_fee", + "type": "u64" + }, + { + "name": "user_quote_amount_in", + "type": "u64" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "user_base_token_account", + "type": "pubkey" + }, + { + "name": "user_quote_token_account", + "type": "pubkey" + }, + { + "name": "protocol_fee_recipient", + "type": "pubkey" + }, + { + "name": "protocol_fee_recipient_token_account", + "type": "pubkey" + }, + { + "name": "coin_creator", + "type": "pubkey" + }, + { + "name": "coin_creator_fee_basis_points", + "type": "u64" + }, + { + "name": "coin_creator_fee", + "type": "u64" + }, + { + "name": "track_volume", + "type": "bool" + }, + { + "name": "total_unclaimed_tokens", + "type": "u64" + }, + { + "name": "total_claimed_tokens", + "type": "u64" + }, + { + "name": "current_sol_volume", + "type": "u64" + }, + { + "name": "last_update_timestamp", + "type": "i64" + }, + { + "name": "min_base_amount_out", + "type": "u64" + }, + { + "name": "ix_name", + "type": "string" + }, + { + "name": "cashback_fee_basis_points", + "type": "u64" + }, + { + "name": "cashback", + "type": "u64" + }, + { + "name": "buyback_fee_basis_points", + "type": "u64" + }, + { + "name": "buyback_fee", + "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "can_boost", + "type": "bool" + }, + { + "name": "base_supply", + "type": "u64" + } + ] + } + }, + { + "name": "ClaimCashbackEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "total_claimed", + "type": "u64" + }, + { + "name": "total_cashback_earned", + "type": "u64" + } + ] + } + }, + { + "name": "ClaimTokenIncentivesEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "total_claimed_tokens", + "type": "u64" + }, + { + "name": "current_sol_volume", + "type": "u64" + } + ] + } + }, + { + "name": "CloseUserVolumeAccumulatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "total_unclaimed_tokens", + "type": "u64" + }, + { + "name": "total_claimed_tokens", + "type": "u64" + }, + { + "name": "current_sol_volume", + "type": "u64" + }, + { + "name": "last_update_timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "CollectCoinCreatorFeeEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "coin_creator", + "type": "pubkey" + }, + { + "name": "coin_creator_fee", + "type": "u64" + }, + { + "name": "coin_creator_vault_ata", + "type": "pubkey" + }, + { + "name": "coin_creator_token_account", + "type": "pubkey" + } + ] + } + }, + { + "name": "ConfigStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Paused" + }, + { + "name": "Active" + } + ] + } + }, + { + "name": "CreateConfigEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "lp_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee_recipients", + "type": { + "array": [ + "pubkey", + 8 + ] + } + }, + { + "name": "coin_creator_fee_basis_points", + "type": "u64" + }, + { + "name": "admin_set_coin_creator_authority", + "type": "pubkey" + } + ] + } + }, + { + "name": "CreatePoolEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "index", + "type": "u16" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "base_mint_decimals", + "type": "u8" + }, + { + "name": "quote_mint_decimals", + "type": "u8" + }, + { + "name": "base_amount_in", + "type": "u64" + }, + { + "name": "quote_amount_in", + "type": "u64" + }, + { + "name": "pool_base_amount", + "type": "u64" + }, + { + "name": "pool_quote_amount", + "type": "u64" + }, + { + "name": "minimum_liquidity", + "type": "u64" + }, + { + "name": "initial_liquidity", + "type": "u64" + }, + { + "name": "lp_token_amount_out", + "type": "u64" + }, + { + "name": "pool_bump", + "type": "u8" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "lp_mint", + "type": "pubkey" + }, + { + "name": "user_base_token_account", + "type": "pubkey" + }, + { + "name": "user_quote_token_account", + "type": "pubkey" + }, + { + "name": "coin_creator", + "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" + } + ] + } + }, + { + "name": "DepositEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "lp_token_amount_out", + "type": "u64" + }, + { + "name": "max_base_amount_in", + "type": "u64" + }, + { + "name": "max_quote_amount_in", + "type": "u64" + }, + { + "name": "user_base_token_reserves", + "type": "u64" + }, + { + "name": "user_quote_token_reserves", + "type": "u64" + }, + { + "name": "pool_base_token_reserves", + "type": "u64" + }, + { + "name": "pool_quote_token_reserves", + "type": "u64" + }, + { + "name": "base_amount_in", + "type": "u64" + }, + { + "name": "quote_amount_in", + "type": "u64" + }, + { + "name": "lp_mint_supply", + "type": "u64" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "user_base_token_account", + "type": "pubkey" + }, + { + "name": "user_quote_token_account", + "type": "pubkey" + }, + { + "name": "user_pool_token_account", + "type": "pubkey" + } + ] + } + }, + { + "name": "DisableEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "disable_create_pool", + "type": "bool" + }, + { + "name": "disable_deposit", + "type": "bool" + }, + { + "name": "disable_withdraw", + "type": "bool" + }, + { + "name": "disable_buy", + "type": "bool" + }, + { + "name": "disable_sell", + "type": "bool" + } + ] + } + }, + { + "name": "ExtendAccountEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "account", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "current_size", + "type": "u64" + }, + { + "name": "new_size", + "type": "u64" + } + ] + } + }, + { + "name": "FeeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "flat_fees", + "type": { + "defined": { + "name": "Fees" + } + } + }, + { + "name": "fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } + }, + { + "name": "stable_fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } + } + ] + } + }, + { + "name": "FeeTier", + "type": { + "kind": "struct", + "fields": [ + { + "name": "market_cap_lamports_threshold", + "type": "u128" + }, + { + "name": "fees", + "type": { + "defined": { + "name": "Fees" + } + } + } + ] + } + }, + { + "name": "Fees", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lp_fee_bps", + "type": "u64" + }, + { + "name": "protocol_fee_bps", + "type": "u64" + }, + { + "name": "creator_fee_bps", + "type": "u64" + } + ] + } + }, + { + "name": "GlobalConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "admin", + "docs": [ + "The admin pubkey" + ], + "type": "pubkey" + }, + { + "name": "lp_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee_basis_points", + "type": "u64" + }, + { + "name": "disable_flags", + "docs": [ + "Flags to disable certain functionality", + "bit 0 - Disable create pool", + "bit 1 - Disable deposit", + "bit 2 - Disable withdraw", + "bit 3 - Disable buy", + "bit 4 - Disable sell" + ], + "type": "u8" + }, + { + "name": "protocol_fee_recipients", + "docs": [ + "Addresses of the protocol fee recipients" + ], + "type": { + "array": [ + "pubkey", + 8 + ] + } + }, + { + "name": "coin_creator_fee_basis_points", + "type": "u64" + }, + { + "name": "admin_set_coin_creator_authority", + "docs": [ + "The admin authority for setting coin creators" + ], + "type": "pubkey" + }, + { + "name": "whitelist_pda", + "type": "pubkey" + }, + { + "name": "reserved_fee_recipient", + "type": "pubkey" + }, + { + "name": "mayhem_mode_enabled", + "type": "bool" + }, + { + "name": "reserved_fee_recipients", + "type": { + "array": [ + "pubkey", + 7 + ] + } + }, + { + "name": "is_cashback_enabled", + "type": "bool" + }, + { + "name": "buyback_fee_recipients", + "type": { + "array": [ + "pubkey", + 8 + ] + } + }, + { + "name": "buyback_basis_points", + "type": "u64" + }, + { + "name": "boost_authority", + "type": "pubkey" + }, + { + "name": "boost_enabled", + "type": "bool" + } + ] + } + }, + { + "name": "GlobalVolumeAccumulator", + "type": { + "kind": "struct", + "fields": [ + { + "name": "start_time", + "type": "i64" + }, + { + "name": "end_time", + "type": "i64" + }, + { + "name": "seconds_in_a_day", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "total_token_supply", + "type": { + "array": [ + "u64", + 30 + ] + } + }, + { + "name": "sol_volumes", + "type": { + "array": [ + "u64", + 30 + ] + } + } + ] + } + }, + { + "name": "InitBoostEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "real_quote_reserves_after", + "type": "u64" + } + ] + } + }, + { + "name": "InitUserVolumeAccumulatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "payer", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "MigratePoolCoinCreatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "sharing_config", + "type": "pubkey" + }, + { + "name": "old_coin_creator", + "type": "pubkey" + }, + { + "name": "new_coin_creator", + "type": "pubkey" + } + ] + } + }, + { + "name": "OptionBool", + "type": { + "kind": "struct", + "fields": [ + "bool" + ] + } + }, + { + "name": "Pool", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_bump", + "type": "u8" + }, + { + "name": "index", + "type": "u16" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "lp_mint", + "type": "pubkey" + }, + { + "name": "pool_base_token_account", + "type": "pubkey" + }, + { + "name": "pool_quote_token_account", + "type": "pubkey" + }, + { + "name": "lp_supply", + "docs": [ + "True circulating supply without burns and lock-ups" + ], + "type": "u64" + }, + { + "name": "coin_creator", + "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" + }, + { + "name": "is_cashback_coin", + "type": "bool" + }, + { + "name": "virtual_quote_reserves", + "docs": [ + "For non-boost pools, value is 0, so the behavior is identical to legacy pools." + ], + "type": "i128" + } + ] + } + }, + { + "name": "ReservedFeeRecipientsEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "reserved_fee_recipient", + "type": "pubkey" + }, + { + "name": "reserved_fee_recipients", + "type": { + "array": [ + "pubkey", + 7 + ] + } + } + ] + } + }, + { + "name": "SellEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "base_amount_in", + "type": "u64" + }, + { + "name": "min_quote_amount_out", + "type": "u64" + }, + { + "name": "user_base_token_reserves", + "type": "u64" + }, + { + "name": "user_quote_token_reserves", + "type": "u64" + }, + { + "name": "pool_base_token_reserves", + "type": "u64" + }, + { + "name": "pool_quote_token_reserves", + "type": "u64" + }, + { + "name": "quote_amount_out", + "type": "u64" + }, + { + "name": "lp_fee_basis_points", + "type": "u64" + }, + { + "name": "lp_fee", + "type": "u64" + }, + { + "name": "protocol_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee", + "type": "u64" + }, + { + "name": "quote_amount_out_without_lp_fee", + "type": "u64" + }, + { + "name": "user_quote_amount_out", + "type": "u64" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "user_base_token_account", + "type": "pubkey" + }, + { + "name": "user_quote_token_account", + "type": "pubkey" + }, + { + "name": "protocol_fee_recipient", + "type": "pubkey" + }, + { + "name": "protocol_fee_recipient_token_account", + "type": "pubkey" + }, + { + "name": "coin_creator", + "type": "pubkey" + }, + { + "name": "coin_creator_fee_basis_points", + "type": "u64" + }, + { + "name": "coin_creator_fee", + "type": "u64" + }, + { + "name": "cashback_fee_basis_points", + "type": "u64" + }, + { + "name": "cashback", + "type": "u64" + }, + { + "name": "buyback_fee_basis_points", + "type": "u64" + }, + { + "name": "buyback_fee", + "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "i128" + }, + { + "name": "can_boost", + "type": "bool" + }, + { + "name": "base_supply", + "type": "u64" + } + ] + } + }, + { + "name": "SetBondingCurveCoinCreatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "coin_creator", + "type": "pubkey" + } + ] + } + }, + { + "name": "SetBoostAuthorityEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "old_boost_authority", + "type": "pubkey" + }, + { + "name": "new_boost_authority", + "type": "pubkey" + } + ] + } + }, + { + "name": "SetMetaplexCoinCreatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "metadata", + "type": "pubkey" + }, + { + "name": "coin_creator", + "type": "pubkey" + } + ] + } + }, + { + "name": "Shareholder", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": "pubkey" + }, + { + "name": "share_bps", + "type": "u16" + } + ] + } + }, + { + "name": "SharingConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "version", + "type": "u8" + }, + { + "name": "status", + "type": { + "defined": { + "name": "ConfigStatus" + } + } + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "admin_revoked", + "type": "bool" + }, + { + "name": "shareholders", + "type": { + "vec": { + "defined": { + "name": "Shareholder" + } + } + } + } + ] + } + }, + { + "name": "SyncUserVolumeAccumulatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "total_claimed_tokens_before", + "type": "u64" + }, + { + "name": "total_claimed_tokens_after", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "UpdateAdminEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "new_admin", + "type": "pubkey" + } + ] + } + }, + { + "name": "UpdateFeeConfigEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "lp_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee_basis_points", + "type": "u64" + }, + { + "name": "protocol_fee_recipients", + "type": { + "array": [ + "pubkey", + 8 + ] + } + }, + { + "name": "coin_creator_fee_basis_points", + "type": "u64" + }, + { + "name": "admin_set_coin_creator_authority", + "type": "pubkey" + } + ] + } + }, + { + "name": "UserVolumeAccumulator", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "needs_claim", + "type": "bool" + }, + { + "name": "total_unclaimed_tokens", + "type": "u64" + }, + { + "name": "total_claimed_tokens", + "type": "u64" + }, + { + "name": "current_sol_volume", + "type": "u64" + }, + { + "name": "last_update_timestamp", + "type": "i64" + }, + { + "name": "has_total_claimed_tokens", + "type": "bool" + }, + { + "name": "cashback_earned", + "type": "u64" + }, + { + "name": "total_cashback_claimed", + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "lp_token_amount_in", + "type": "u64" + }, + { + "name": "min_base_amount_out", + "type": "u64" + }, + { + "name": "min_quote_amount_out", + "type": "u64" + }, + { + "name": "user_base_token_reserves", + "type": "u64" + }, + { + "name": "user_quote_token_reserves", + "type": "u64" + }, + { + "name": "pool_base_token_reserves", + "type": "u64" + }, + { + "name": "pool_quote_token_reserves", + "type": "u64" + }, + { + "name": "base_amount_out", + "type": "u64" + }, + { + "name": "quote_amount_out", + "type": "u64" + }, + { + "name": "lp_mint_supply", + "type": "u64" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "user_base_token_account", + "type": "pubkey" + }, + { + "name": "user_quote_token_account", + "type": "pubkey" + }, + { + "name": "user_pool_token_account", + "type": "pubkey" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/crates/core/src/scenarios/protocols/pump-amm/v1/overrides.yaml b/crates/core/src/scenarios/protocols/pump-amm/v1/overrides.yaml new file mode 100644 index 000000000..796ce4692 --- /dev/null +++ b/crates/core/src/scenarios/protocols/pump-amm/v1/overrides.yaml @@ -0,0 +1,165 @@ +protocol: PumpSwap +version: v1 +account_type: Pool +idl_file_path: idl.json + +tags: + - amm + - constant-product + - defi + +constants: + # Token mints loaded from verified tokens registry + token_mint: + label: Token + description: Select the base token mint (the pump.fun coin) from verified tokens + source: verified_tokens + address_suffix: pump + +templates: + - id: pump-amm-pool-state + name: Override Pool State + description: Override any PumpSwap pool by specifying its address directly + idl_account_name: Pool + properties: + - path: lp_supply + label: LP Supply + description: Total LP token supply before user burns and lock-ups + - path: coin_creator + label: Coin Creator + description: Pubkey accruing the coin-creator fee for this pool + - path: virtual_quote_reserves + label: Virtual Quote Reserves + description: Appended quote reserves added to the quote vault balance when quoting (0 on all pools today) + llm_context: | + Pass the pool's own address; unlike pump-amm-canonical-pool this template derives + nothing. Use it for user-created or non-WSOL pools, or any pool whose address you + already have. Set fetchBeforeUse: true so the fields you don't override keep their + live values. + + PumpSwap is a constant-product AMM whose reserves live in the pool's token accounts, + not in the Pool account: price = effective_quote_reserves / base_vault_balance, where + effective_quote_reserves = pool_quote_token_account balance + Pool.virtual_quote_reserves. + Move price by setting virtual_quote_reserves here, or by editing the vault balances with + the spl-token template. + address: + type: pubkey + + - id: pump-amm-canonical-pool + name: Override Canonical Pool (Custom) + description: | + Override the canonical PumpSwap pool of a migrated pump.fun coin by specifying its mint. + Covers WSOL-quoted canonical migrations only; for any other pool pass its address to + pump-amm-pool-state. Two PDAs are derived: + - Pool authority (Pump program): ["pool-authority", base_mint] + - Pool (PumpSwap): ["pool", index 0 as u16 LE, pool_authority, base_mint, WSOL] + idl_account_name: Pool + properties: + - path: lp_supply + label: LP Supply + description: Total LP token supply before user burns and lock-ups + - path: coin_creator + label: Coin Creator + description: Pubkey accruing the coin-creator fee for this pool + - path: virtual_quote_reserves + label: Virtual Quote Reserves + description: Appended quote reserves added to the quote vault balance when quoting (0 on all pools today) + - path: base_mint + type: constant_ref + label: Base Token Mint + constant: token_mint + llm_context: | + Set fetchBeforeUse: true so the fields you don't override keep their live values. + Use false only for a later override that builds on state an earlier one prepared in + the same scenario. + + WORKS ONLY FOR MIGRATED PUMP.FUN COINS. Coins that completed before PumpSwap + launched (March 2025) migrated to Raydium and have no canonical pool. The + canonical pool (index 0) is created by the Pump program's migrate instruction: its creator seed is the Pump program's + pool-authority PDA ["pool-authority", base_mint] and its quote mint is always + wrapped SOL — this template cannot derive pools quoted in any other mint. Pools + created directly by users carry the creator's own pubkey and possibly a different + index — override those (and any non-WSOL pool) with the pump-amm-pool-state + template by passing the pool address directly. + + PRICING: PumpSwap is a constant-product AMM whose reserves live in the pool's token + accounts, NOT in the Pool account: + - price = effective_quote_reserves / base_vault_balance (coin has 6 decimals, SOL 9) + - effective_quote_reserves = pool_quote_token_account balance + Pool.virtual_quote_reserves + + TO SIMULATE PRICE CHANGES either: + 1. modify the vault token balances with the spl-token template — the vault addresses + are stored in the Pool account's pool_base_token_account and + pool_quote_token_account fields, or + 2. set virtual_quote_reserves on this Pool account — it shifts the effective quote + reserves without touching any token balance. + + EXAMPLE - "make a migrated coin ~10x more expensive to buy": read the pool's quote + vault balance Q (getTokenAccountBalance on pool_quote_token_account), then one + override at slot 0, fetchBeforeUse: true, values = + base_mint: + virtual_quote_reserves: <9 * Q> + Effective quote reserves become Q + 9*Q = 10*Q, so the same buy costs about 10x + what it did before, and a buy sized to the old price fails with ExceededSlippage + (0x1774). Use spl-token on the vaults instead when you want the balances, not just + the quote, to move. + + FEES per swap: trades read the fee program's FeeConfig market-cap fee tiers — a + required account of every buy and sell; GlobalConfig's flat lp_fee_basis_points + + protocol_fee_basis_points + coin_creator_fee_basis_points are legacy fields from + before the external fee program. + address: + type: pda + program_id: pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA + seeds: + - type: string + value: pool + - type: u16_le + value: 0 + - type: derived_pda + program_id: 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P + seeds: + - type: string + value: pool-authority + - type: property_ref + value: base_mint + - type: property_ref + value: base_mint + - type: pubkey + value: So11111111111111111111111111111111111111112 + + - id: pump-amm-global-config + name: Override Global Config + description: | + Override the PumpSwap program's single GlobalConfig account + (PDA derived from ["global_config"], resolving to ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw). + idl_account_name: GlobalConfig + properties: + - path: lp_fee_basis_points + label: LP Fee Basis Points (legacy) + description: Legacy LP fee in basis points (deposits and withdrawals are free); live trades read fees from the fee program's FeeConfig + - path: protocol_fee_basis_points + label: Protocol Fee Basis Points (legacy) + description: Legacy protocol fee in basis points, superseded by the external fee program + - path: coin_creator_fee_basis_points + label: Coin Creator Fee Basis Points (legacy) + description: Legacy coin-creator fee in basis points; accrues to each pool's coin_creator + - path: disable_flags + label: Disable Flags + description: Bitmask disabling individual instructions (0 = everything enabled) + llm_context: | + Set fetchBeforeUse: true so GlobalConfig's admin and protocol fee recipient list keep + their live values; false only to build on an earlier override's prepared state. + + lp_fee_basis_points, protocol_fee_basis_points and coin_creator_fee_basis_points + are legacy fields from before the external fee program (deposits and withdrawals + are free); trades read fees from the fee program's FeeConfig market-cap fee tiers + — a required account of every buy and sell. coin_creator_fee_basis_points accrues + to each pool's coin_creator. disable_flags is a bitmask disabling individual + instructions (0 = everything enabled). + address: + type: pda + program_id: pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA + seeds: + - type: string + value: global_config diff --git a/crates/core/src/scenarios/protocols/pump/README.md b/crates/core/src/scenarios/protocols/pump/README.md new file mode 100644 index 000000000..7b0aeb930 --- /dev/null +++ b/crates/core/src/scenarios/protocols/pump/README.md @@ -0,0 +1,209 @@ +# Pump / PumpSwap state preparation + +Declarative state-preparation templates for the pump.fun ecosystem. A pump.fun coin's +life spans two on-chain programs, so the integration covers both: + +1. **Pump** (bonding curve launchpad) — new coins trade on a constant-product curve over + synthetic (virtual) reserves until the curve is bought out (`complete = true`). +2. **PumpSwap** (`pump-amm`, pump.fun's AMM) — completed curves migrate here; reserves + live in the pool's token accounts, not in the Pool account itself. + +Both programs publish Anchor IDLs, so the templates use the standard IDL override path +(no raw-offset layout needed). + +## Program identity (verified 2026-08-07) + +| | Pump | PumpSwap | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| Program ID | `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` | `pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA` | +| ProgramData | `B5MvUwXdiW1NMM6QFFD3ssPKBujD4zMohncbM73Z2BQu` | `6naEzKeUuFh1Jeeu51NXQgr5qkXgXtc9WKNct4xynVJc` | +| Last deployed slot | 433095571 | 433112355 | +| Bundled IDL | byte-identical to [`idl/pump.json`](https://github.com/pump-fun/pump-public-docs/blob/main/idl/pump.json) at pump-public-docs commit `3c6721a67c0b` | byte-identical to [`idl/pump_amm.json`](https://github.com/pump-fun/pump-public-docs/blob/main/idl/pump_amm.json) at commit `2c22246b6708` | + +A later deployment slot than the one above means the program was upgraded and this +integration must be revisited (layouts, formulas, fee wiring). + +Fees: every buy/sell passes the fee program's `FeeConfig` (market-cap fee tiers) as a +required account under `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ`; the flat +basis-point fields on `Global` / `GlobalConfig` are legacy. + +## Templates + +| Template | Account | Address | Use for | +| --------------------------- | -------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `pump-bonding-curve-custom` | `BondingCurve` | PDA `["bonding-curve", mint]` | any coin's curve, selected by mint | +| `pump-global` | `Global` | PDA `["global"]` → `4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf` | fee/init parameters for curves created after the override | +| `pump-amm-pool-state` | `Pool` | caller-provided pubkey | any pool by raw address (only path for non-canonical / non-WSOL pools) | +| `pump-amm-canonical-pool` | `Pool` | PDA `["pool", u16le(0), PDA(pump, ["pool-authority", mint]), mint, WSOL]` | the canonical WSOL-quoted pool of a migrated coin, selected by mint | +| `pump-amm-global-config` | `GlobalConfig` | PDA `["global_config"]` → `ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw` | pool fee/disable flags | + +Notes: + +- The mint catalogs offer only verified tokens whose address ends in `pump` + (976 mints in the bundled catalog; `address_suffix: pump` in both `overrides.yaml` + files). Coins outside the verified catalog — including freshly launched ones — are + still reachable through the graduation MCP tool and its Studio preset, which validate + live on-chain state instead of the catalog, or through the raw REST payload, which + performs no catalog validation. +- `pump-amm-canonical-pool` covers WSOL-quoted canonical migrations only; any other + pool goes through `pump-amm-pool-state` with its address. +- Always set `fetchBeforeUse: true` so non-overridden fields keep their live values. + +## Field reference + +What each overridable field means and what overriding it lets you model. Keep the +curve-lifetime invariants when you touch reserves (`virtual − real` = 279.9T tokens and +30 SOL of quote with today's mainnet `Global` defaults). + +### `BondingCurve` + +| Field | Meaning | Override it to | +| ------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `virtual_token_reserves` | Synthetic token reserves in the price formula (raw, 6 decimals) | reprice the curve - spot = `virtual_quote / virtual_token` | +| `virtual_quote_reserves` | Synthetic quote reserves (lamports for SOL-quoted coins) | reprice the curve | +| `real_token_reserves` | Tokens the curve still holds; completion is when this reaches 0 | set how close to graduation the curve sits (a small value = one buy away) | +| `real_quote_reserves` | Quote the curve actually holds | model accumulated quote | +| `complete` | True once bought out; a completed curve rejects buy/sell and can only migrate | flip `true` to model a graduated curve, `false` to reopen trading | +| `creator` | Coin creator that accrues creator fees via the creator vault | point creator fees at a key you control | +| `token_mint` | Selects which coin's curve (a PDA seed, not a stored field) | choose the target coin | + +### `Global` (singleton, `["global"]`) + +| Field | Meaning | Override it to | +| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `fee_basis_points`, `creator_fee_basis_points` | Legacy flat fees; live trades read the fee program's `FeeConfig` | rarely useful (legacy) | +| `initial_virtual_token_reserves`, `initial_virtual_sol_reserves`, `initial_virtual_quote_reserves`, `initial_real_token_reserves`, `token_total_supply` | Seed values a **new** curve is created with | change the launch parameters of curves created after the override (existing curves keep theirs) | +| `enable_migrate` | Gates the `migrate` instruction | set `true` to let a completed curve migrate to PumpSwap | +| `pool_migration_fee` | Fee charged when a curve migrates | model migration cost | +| `withdraw_authority` | Authority the `migrate` / withdraw path checks | set to a key you control to drive a real `migrate` transaction on a fork | + +## Worked example: reset a curve to a fresh state + +Create it in the Studio editor (Pump tile → _Override Bonding Curve (Custom)_), which +fills the envelope automatically, or POST the full REST `Scenario` shape below to +`/v1/scenarios` — every envelope field is required by the endpoint, and the `account` +derivation is copied verbatim from the template. Then press Play: + +```json +{ + "id": "d2f8a1c4-7b3e-4e9a-8c5d-0f6b2a9e4d71", + "name": "fresh pump curve", + "description": "reset a live coin's curve to launch state", + "tags": ["pump"], + "overrides": [ + { + "id": "curve-reset-0", + "templateId": "pump-bonding-curve-custom", + "label": "reset bonding curve to launch state", + "enabled": true, + "scenarioRelativeSlot": 0, + "fetchBeforeUse": true, + "account": { + "pda": { + "programId": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + "seeds": [ + { "string": "bonding-curve" }, + { "propertyRef": "token_mint" } + ] + } + }, + "values": { + "token_mint": "", + "virtual_token_reserves": 1073000000000000, + "virtual_quote_reserves": 30000000000, + "real_token_reserves": 793100000000000, + "real_quote_reserves": 0, + "complete": false + } + } + ] +} +``` + +Expected result: the coin's `BondingCurve` PDA holds exactly these values while every +other byte (token_total_supply, creator, the extend_account tail) stays live. Verify by +re-opening the override in the Studio field editor, or via `getAccountInfo`: u64 LE at +offsets 8 (virtual tokens), 16 (virtual quote), 24 (real tokens), 32 (real quote), bool +at 48 (complete). The surfpool log must contain no `skipping override` line. + +Variant — a semantically valid _completed_ curve (rejects buys/sells with +`BondingCurveComplete`): `complete: true` requires `real_token_reserves: 0`; keep the +curve-lifetime invariants (`virtual − real` = 279.9T tokens / 30 SOL of quote, verified +against live mainnet data). + +## Prepare a graduation + +The `create_pump_graduation_scenario` MCP tool builds the whole preparation from Surfnet +state, with no field math required from the caller. Accounts already present locally are +authoritative; Surfnet fetches only missing accounts from mainnet. The same tool powers the +_Pump Graduation_ preset card in Studio. + +```json +{ + "tokenMint": "" +} +``` + +The coin must have a **Token-2022 mint**, a SOL-quoted incomplete curve, and no canonical +PumpSwap pool yet. Eligibility failures are returned in the MCP tool result. The preset +does not cover coins with a classic SPL-Token mint or a non-SOL quote mint — their curve +can still be overridden field by field with `pump-bonding-curve-custom`, but this +graduation flow is Token-2022 and SOL-quote only. A successful tool call +returns the Studio editor URL, where the stored scenario contains three overrides: + +1. the curve one buy away from completion (`real_token_reserves` = the finishing buy, + sized so the buy also clears the migration fee), +2. the curve vault topped up to `migration reserve + finishing buy` — the reserve is + what `migrate_v2` moves into the pool, so draining the vault to match + `real_token_reserves` would make migration fail with `ZeroBaseAmount`, +3. `Global.enable_migrate = true`. + +Press Play, then drive it like a user would: a real `buy_v2` of the curve override's +`real_token_reserves` completes the curve, and a real `migrate_v2` creates the canonical +WSOL pool with the reserve as its base liquidity. + +## Shock a migrated pool's price + +Use the existing `pump-amm-canonical-pool` template through the standard scenario API or +the generic `create_scenario` MCP tool. The Studio preset follows the same path: it loads +the registered template, supplies the mint and reserve value, and stores a normal scenario. + +```json +{ + "templateId": "pump-amm-canonical-pool", + "values": { + "base_mint": "", + "virtual_quote_reserves": 15000000000000 + }, + "scenarioRelativeSlot": 1, + "fetchBeforeUse": true +} +``` + +`virtual_quote_reserves` is appended to the quote vault balance +when the AMM quotes, so raising it makes the same sell return more quote without +touching any token balance. The template derives the canonical WSOL pool from `base_mint`. +If the derived account is absent or invalid, Play reports the materialization failure. +After a successful Play, the identical sell transaction simulates with a higher quote-token +output than before. + +## Verification + +- Address identity: `cargo test -p surfpool-core --lib pump` — template PDAs pinned to + externally documented addresses (pump-public-docs). +- MCP surface: `cargo test -p surfpool-mcp surfpool::tests` — compact template listing, + catalog scoping, and generic scenario validation. +- Integration: `cargo test -p surfpool-core --features integration-tests pump` needs a + network connection (`SURFPOOL_TEST_RPC_URL` overrides the public mainnet endpoint). + It round-trips live mainnet curve, pool, and config accounts through the bundled + IDLs to catch layout drift after a program upgrade, proves overrides touch only + their target bytes on real account data, and exercises the graduation builder's + validation against live state. The lifecycle test discovers a fresh, still-trading + Token-2022 coin from the pump program's recent transactions (no fixed coin stays + incomplete; the fork freezes its state on first read), prepares it through the + production builder and materializer, then executes real `buy_v2` and `migrate_v2` + against the live programs. The price-shock test compares a baseline and a shocked + PumpSwap `sell` simulation on the established live canonical pool and requires the + quote-token output to change, then executes the sell for real. Trading against a + freshly migrated (cashback-era) pool uses a different fee wiring and is an open + follow-up. diff --git a/crates/core/src/scenarios/protocols/pump/mod.rs b/crates/core/src/scenarios/protocols/pump/mod.rs new file mode 100644 index 000000000..a3a6d96c3 --- /dev/null +++ b/crates/core/src/scenarios/protocols/pump/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/crates/core/src/scenarios/protocols/pump/v1/graduation_builder.rs b/crates/core/src/scenarios/protocols/pump/v1/graduation_builder.rs new file mode 100644 index 000000000..4c20150e3 --- /dev/null +++ b/crates/core/src/scenarios/protocols/pump/v1/graduation_builder.rs @@ -0,0 +1,406 @@ +use std::collections::HashMap; + +use solana_account::Account; +use solana_pubkey::Pubkey; +use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; +use surfpool_types::{AccountAddress, OverrideInstance, Scenario}; + +use crate::{ + error::{SurfpoolError, SurfpoolResult}, + scenarios::TemplateRegistry, + types::TokenAccount, +}; + +const PUMP_PROGRAM_ID: Pubkey = + Pubkey::from_str_const("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); +const TOKEN_2022_PROGRAM_ID: Pubkey = + Pubkey::from_str_const("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); + +const VIRTUAL_TOKEN_RESERVES_OFFSET: usize = 8; +const VIRTUAL_QUOTE_RESERVES_OFFSET: usize = 16; +const REAL_TOKEN_RESERVES_OFFSET: usize = 24; +const REAL_QUOTE_RESERVES_OFFSET: usize = 32; +const COMPLETE_OFFSET: usize = 48; +const QUOTE_MINT_OFFSET: usize = 83; +const POOL_MIGRATION_FEE_OFFSET: usize = 146; +const GRADUATION_PREPARATION_SLOT: u64 = 1; +const MIGRATION_FEE_BUFFER: u64 = 3; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PumpGraduationAddresses { + pub bonding_curve: Pubkey, + pub curve_vault: Pubkey, + pub canonical_pool: Pubkey, + pub global: Pubkey, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PumpGraduationPreparation { + pub scenario: Scenario, + pub token_mint: Pubkey, + pub addresses: PumpGraduationAddresses, + pub completing_buy_amount: u64, + pub migration_reserve: u64, +} + +/// Resolved from the templates' PDA specs, so the builder reads the addresses +/// the materializer writes to. +pub fn pump_graduation_addresses(token_mint: &Pubkey) -> SurfpoolResult { + let registry = TemplateRegistry::new(); + let resolve = |template_id: &str, mint_property: Option<&str>| { + let template = registry.get(template_id).ok_or_else(|| { + SurfpoolError::internal(format!("{template_id} template is unavailable")) + })?; + let values = mint_property.map(|property| { + HashMap::from([( + property.to_string(), + serde_json::json!(token_mint.to_string()), + )]) + }); + template.address.resolve(values.as_ref()).ok_or_else(|| { + SurfpoolError::internal(format!("{template_id} address does not resolve")) + }) + }; + + let bonding_curve = resolve("pump-bonding-curve-custom", Some("token_mint"))?; + Ok(PumpGraduationAddresses { + bonding_curve, + curve_vault: get_associated_token_address_with_program_id( + &bonding_curve, + token_mint, + &TOKEN_2022_PROGRAM_ID, + ), + canonical_pool: resolve("pump-amm-canonical-pool", Some("base_mint"))?, + global: resolve("pump-global", None)?, + }) +} + +pub fn build_pump_graduation_scenario( + token_mint: Pubkey, + mint_account: &Account, + curve_account: &Account, + curve_vault_account: &Account, + canonical_pool_account: Option<&Account>, + global_account: &Account, +) -> SurfpoolResult { + validate_accounts( + token_mint, + mint_account, + curve_account, + curve_vault_account, + canonical_pool_account, + global_account, + )?; + + let virtual_token_reserves = read_u64( + &curve_account.data, + VIRTUAL_TOKEN_RESERVES_OFFSET, + "virtual_token_reserves", + )?; + let virtual_quote_reserves = read_u64( + &curve_account.data, + VIRTUAL_QUOTE_RESERVES_OFFSET, + "virtual_quote_reserves", + )?; + let real_token_reserves = read_u64( + &curve_account.data, + REAL_TOKEN_RESERVES_OFFSET, + "real_token_reserves", + )?; + let real_quote_reserves = read_u64( + &curve_account.data, + REAL_QUOTE_RESERVES_OFFSET, + "real_quote_reserves", + )?; + let token_offset = virtual_token_reserves + .checked_sub(real_token_reserves) + .ok_or_else(|| invalid_curve("virtual token reserves are below real token reserves"))?; + virtual_quote_reserves + .checked_sub(real_quote_reserves) + .ok_or_else(|| invalid_curve("virtual quote reserves are below real quote reserves"))?; + + let token_account = TokenAccount::unpack(&curve_vault_account.data)?; + let migration_reserve = token_account + .amount() + .checked_sub(real_token_reserves) + .ok_or_else(|| invalid_curve("curve vault balance is below real token reserves"))?; + if migration_reserve == 0 { + return Err(invalid_curve("curve vault has no migration reserve")); + } + + let pool_migration_fee = read_u64( + &global_account.data, + POOL_MIGRATION_FEE_OFFSET, + "pool_migration_fee", + )?; + let target_final_quote = pool_migration_fee + .checked_mul(MIGRATION_FEE_BUFFER) + .ok_or_else(|| invalid_curve("pool migration fee overflows"))?; + let required_quote_in = target_final_quote + .saturating_sub(real_quote_reserves) + .max(1); + let prepared_real_quote_reserves = real_quote_reserves; + let prepared_virtual_quote_reserves = virtual_quote_reserves; + let completing_buy_amount = div_ceil( + u128::from(required_quote_in) * u128::from(token_offset), + u128::from(prepared_virtual_quote_reserves), + )?; + let completing_buy_amount = u64::try_from(completing_buy_amount) + .map_err(|_| invalid_curve("completing buy amount does not fit in u64"))?; + if completing_buy_amount == 0 || completing_buy_amount > real_token_reserves { + return Err(invalid_curve( + "curve does not have enough real token reserves for a migration-safe finishing buy", + )); + } + + let prepared_virtual_token_reserves = token_offset + .checked_add(completing_buy_amount) + .ok_or_else(|| invalid_curve("virtual token reserves overflow"))?; + let prepared_vault_amount = migration_reserve + .checked_add(completing_buy_amount) + .ok_or_else(|| invalid_curve("curve vault amount overflow"))?; + let registry = TemplateRegistry::new(); + let curve_template = registry + .get("pump-bonding-curve-custom") + .ok_or_else(|| SurfpoolError::internal("pump bonding curve template is unavailable"))?; + let vault_template = registry + .get("spl-token-account-balance") + .ok_or_else(|| SurfpoolError::internal("SPL token balance template is unavailable"))?; + let global_template = registry + .get("pump-global") + .ok_or_else(|| SurfpoolError::internal("pump Global template is unavailable"))?; + let mint = token_mint.to_string(); + + let curve_values = HashMap::from([ + ("token_mint".to_string(), serde_json::json!(mint)), + ( + "virtual_token_reserves".to_string(), + serde_json::json!(prepared_virtual_token_reserves), + ), + ( + "virtual_quote_reserves".to_string(), + serde_json::json!(prepared_virtual_quote_reserves), + ), + ( + "real_token_reserves".to_string(), + serde_json::json!(completing_buy_amount), + ), + ( + "real_quote_reserves".to_string(), + serde_json::json!(prepared_real_quote_reserves), + ), + ("complete".to_string(), serde_json::json!(false)), + ]); + let vault_values = HashMap::from([( + "amount".to_string(), + serde_json::json!(prepared_vault_amount), + )]); + let global_values = HashMap::from([("enable_migrate".to_string(), serde_json::json!(true))]); + let curve_override = OverrideInstance::new( + curve_template.id.clone(), + GRADUATION_PREPARATION_SLOT, + curve_template.address.clone(), + ) + .with_values(curve_values) + .with_label("Near-complete bonding curve".to_string()); + // The generic template takes an explicit address, so the derived vault is filled in here. + let addresses = pump_graduation_addresses(&token_mint)?; + let vault_override = OverrideInstance::new( + vault_template.id.clone(), + GRADUATION_PREPARATION_SLOT, + AccountAddress::Pubkey(addresses.curve_vault.to_string()), + ) + .with_values(vault_values) + .with_label("Migration-safe curve vault".to_string()); + let global_override = OverrideInstance::new( + global_template.id.clone(), + GRADUATION_PREPARATION_SLOT, + global_template.address.clone(), + ) + .with_values(global_values) + .with_label("Migration enabled".to_string()); + + let mut scenario = Scenario::new( + "Pump Graduation".to_string(), + "Prepare a SOL-quoted Token-2022 pump.fun curve for one finishing buy and migration to PumpSwap." + .to_string(), + ); + scenario.tags = vec!["pump".to_string(), "graduation".to_string()]; + scenario.add_override(curve_override); + scenario.add_override(vault_override); + scenario.add_override(global_override); + + Ok(PumpGraduationPreparation { + scenario, + token_mint, + addresses, + completing_buy_amount, + migration_reserve, + }) +} + +fn validate_accounts( + token_mint: Pubkey, + mint_account: &Account, + curve_account: &Account, + curve_vault_account: &Account, + canonical_pool_account: Option<&Account>, + global_account: &Account, +) -> SurfpoolResult<()> { + if mint_account.owner != TOKEN_2022_PROGRAM_ID { + return Err(invalid_curve("mint is not owned by Token-2022")); + } + if curve_account.owner != PUMP_PROGRAM_ID { + return Err(invalid_curve("bonding curve is not owned by pump")); + } + if curve_account.data.get(COMPLETE_OFFSET) != Some(&0) { + return Err(invalid_curve("bonding curve is already complete")); + } + if read_pubkey(&curve_account.data, QUOTE_MINT_OFFSET, "quote_mint")? != Pubkey::default() { + return Err(invalid_curve( + "Pump graduation preset supports SOL-quoted bonding curves only", + )); + } + if canonical_pool_account.is_some() { + return Err(invalid_curve("canonical PumpSwap pool already exists")); + } + if curve_vault_account.owner != TOKEN_2022_PROGRAM_ID { + return Err(invalid_curve("curve vault is not owned by Token-2022")); + } + let token_account = TokenAccount::unpack(&curve_vault_account.data)?; + if token_account.mint() != token_mint { + return Err(invalid_curve("curve vault contains a different mint")); + } + if global_account.owner != PUMP_PROGRAM_ID { + return Err(invalid_curve("pump Global account has the wrong owner")); + } + Ok(()) +} + +fn read_u64(data: &[u8], offset: usize, field: &str) -> SurfpoolResult { + let bytes = data + .get(offset..offset + 8) + .ok_or_else(|| invalid_curve(format!("missing {field}")))?; + Ok(u64::from_le_bytes(bytes.try_into().unwrap())) +} + +fn read_pubkey(data: &[u8], offset: usize, field: &str) -> SurfpoolResult { + let bytes = data + .get(offset..offset + 32) + .ok_or_else(|| invalid_curve(format!("missing {field}")))?; + Pubkey::try_from(bytes).map_err(|_| invalid_curve(format!("invalid {field}"))) +} + +fn div_ceil(numerator: u128, denominator: u128) -> SurfpoolResult { + if denominator == 0 { + return Err(invalid_curve("division by zero")); + } + Ok(numerator.div_ceil(denominator)) +} + +fn invalid_curve(message: impl Into) -> SurfpoolError { + SurfpoolError::internal(message.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn write_u64(data: &mut [u8], offset: usize, value: u64) { + data[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + + #[test] + fn graduation_preserves_quote_reserves_and_sizes_only_the_shortfall() { + let token_mint = Pubkey::new_unique(); + let token_offset = 1_000; + let real_token_reserves = 500; + let virtual_token_reserves = token_offset + real_token_reserves; + let real_quote_reserves = 200; + let virtual_quote_reserves = 500; + let pool_migration_fee = 100; + let migration_reserve = 50; + + let mint_account = Account { + owner: TOKEN_2022_PROGRAM_ID, + ..Account::default() + }; + let mut curve_data = vec![0; QUOTE_MINT_OFFSET + 32]; + write_u64( + &mut curve_data, + VIRTUAL_TOKEN_RESERVES_OFFSET, + virtual_token_reserves, + ); + write_u64( + &mut curve_data, + VIRTUAL_QUOTE_RESERVES_OFFSET, + virtual_quote_reserves, + ); + write_u64( + &mut curve_data, + REAL_TOKEN_RESERVES_OFFSET, + real_token_reserves, + ); + write_u64( + &mut curve_data, + REAL_QUOTE_RESERVES_OFFSET, + real_quote_reserves, + ); + let curve_account = Account { + owner: PUMP_PROGRAM_ID, + data: curve_data, + ..Account::default() + }; + + let mut vault = TokenAccount::new( + &TOKEN_2022_PROGRAM_ID, + Pubkey::new_unique(), + token_mint, + None, + ); + vault.set_amount(real_token_reserves + migration_reserve); + let curve_vault_account = Account { + owner: TOKEN_2022_PROGRAM_ID, + data: vault.pack_into_vec(), + ..Account::default() + }; + + let mut global_data = vec![0; POOL_MIGRATION_FEE_OFFSET + 8]; + write_u64( + &mut global_data, + POOL_MIGRATION_FEE_OFFSET, + pool_migration_fee, + ); + let global_account = Account { + owner: PUMP_PROGRAM_ID, + data: global_data, + ..Account::default() + }; + + let preparation = build_pump_graduation_scenario( + token_mint, + &mint_account, + &curve_account, + &curve_vault_account, + None, + &global_account, + ) + .expect("valid graduation state"); + + assert_eq!(preparation.completing_buy_amount, 200); + let curve_override = preparation + .scenario + .overrides + .iter() + .find(|instance| instance.template_id == "pump-bonding-curve-custom") + .expect("curve override"); + assert_eq!( + curve_override.values["real_quote_reserves"], + serde_json::json!(real_quote_reserves) + ); + assert_eq!( + curve_override.values["virtual_quote_reserves"], + serde_json::json!(virtual_quote_reserves) + ); + } +} diff --git a/crates/core/src/scenarios/protocols/pump/v1/idl.json b/crates/core/src/scenarios/protocols/pump/v1/idl.json new file mode 100644 index 000000000..062e66f03 --- /dev/null +++ b/crates/core/src/scenarios/protocols/pump/v1/idl.json @@ -0,0 +1,6813 @@ +{ + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + "metadata": { + "name": "pump", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "add_quote_mint", + "discriminator": [111, 121, 21, 56, 40, 24, 94, 209], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "quote_mint", + "type": "pubkey" + } + ] + }, + { + "name": "admin_set_creator", + "docs": [ + "Allows Global::admin_set_creator_authority to override the bonding curve creator" + ], + "discriminator": [69, 25, 171, 142, 57, 239, 13, 4], + "accounts": [ + { + "name": "admin_set_creator_authority", + "signer": true, + "relations": ["global"] + }, + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "mint" + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "creator", + "type": "pubkey" + } + ] + }, + { + "name": "admin_set_idl_authority", + "discriminator": [8, 217, 96, 231, 144, 104, 192, 5], + "accounts": [ + { + "name": "authority", + "signer": true, + "relations": ["global"] + }, + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "idl_account", + "writable": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "program_signer", + "pda": { + "seeds": [] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "idl_authority", + "type": "pubkey" + } + ] + }, + { + "name": "admin_update_token_incentives", + "discriminator": [209, 11, 115, 87, 213, 23, 124, 204], + "accounts": [ + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "global_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + } + ] + } + }, + { + "name": "mint" + }, + { + "name": "global_incentive_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "global_volume_accumulator" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "start_time", + "type": "i64" + }, + { + "name": "end_time", + "type": "i64" + }, + { + "name": "seconds_in_a_day", + "type": "i64" + }, + { + "name": "day_number", + "type": "u64" + }, + { + "name": "pump_token_supply_per_day", + "type": "u64" + } + ] + }, + { + "name": "buy", + "docs": ["Buys tokens from a bonding curve."], + "discriminator": [102, 6, 61, 18, 1, 218, 235, 234], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "mint" + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "associated_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "associated_user", + "writable": true + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program" + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] + }, + { + "kind": "const", + "value": [ + 1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, + 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, + 182, 253, 109, 24, 176 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "max_sol_cost", + "type": "u64" + }, + { + "name": "track_volume", + "type": { + "defined": { + "name": "OptionBool" + } + } + } + ] + }, + { + "name": "buy_exact_quote_in_v2", + "discriminator": [194, 171, 28, 70, 104, 77, 91, 47], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "associated_quote_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "buyback_fee_recipient", + "writable": true + }, + { + "name": "associated_quote_buyback_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "buyback_fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "associated_base_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "associated_quote_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "associated_base_user", + "writable": true + }, + { + "name": "associated_quote_user", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "associated_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "sharing_config", + "docs": [ + "seeds; the account is intentionally not deserialized here because it may be uninitialized", + "for mints that have not created a fee sharing config. Handlers must check", + "`data_is_empty()` / owner before reading." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "associated_user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] + }, + { + "kind": "const", + "value": [ + 1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, + 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, + 182, 253, 109, 24, 176 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [ + { + "name": "spendable_quote_in", + "type": "u64" + }, + { + "name": "min_tokens_out", + "type": "u64" + } + ] + }, + { + "name": "buy_exact_sol_in", + "docs": [ + "Given a budget of spendable SOL, buy at least min_tokens_out tokens.", + "Fees are deducted from spendable_sol_in.", + "", + "# Quote formulas", + "Where:", + "- total_fee_bps = protocol_fee_bps + creator_fee_bps (creator_fee_bps is 0 if no creator)", + "- floor(a/b) = a / b (integer division)", + "- ceil(a/b) = (a + b - 1) / b", + "", + "SOL → tokens quote", + "To calculate tokens_out for a given spendable_sol_in:", + "1. net_sol = floor(spendable_sol_in * 10_000 / (10_000 + total_fee_bps))", + "2. fees = ceil(net_sol * protocol_fee_bps / 10_000) + ceil(net_sol * creator_fee_bps / 10_000) (creator_fee_bps is 0 if no creator)", + "3. if net_sol + fees > spendable_sol_in: net_sol = net_sol - (net_sol + fees - spendable_sol_in)", + "4. tokens_out = floor((net_sol - 1) * virtual_token_reserves / (virtual_sol_reserves + net_sol - 1))", + "", + "Reverse quote (tokens → SOL)", + "To calculate spendable_sol_in for a desired number of tokens:", + "1. net_sol = ceil(tokens * virtual_sol_reserves / (virtual_token_reserves - tokens)) + 1", + "2. spendable_sol_in = ceil(net_sol * (10_000 + total_fee_bps) / 10_000)", + "", + "Rent", + "Separately make sure the instruction's payer has enough SOL to cover rent for:", + "- creator_vault: rent.minimum_balance(0)", + "- user_volume_accumulator: rent.minimum_balance(UserVolumeAccumulator::LEN)" + ], + "discriminator": [56, 252, 116, 8, 158, 223, 205, 95], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "mint" + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "associated_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "associated_user", + "writable": true + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program" + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] + }, + { + "kind": "const", + "value": [ + 1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, + 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, + 182, 253, 109, 24, 176 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + } + ], + "args": [ + { + "name": "spendable_sol_in", + "type": "u64" + }, + { + "name": "min_tokens_out", + "type": "u64" + }, + { + "name": "track_volume", + "type": { + "defined": { + "name": "OptionBool" + } + } + } + ] + }, + { + "name": "buy_v2", + "discriminator": [184, 23, 238, 97, 103, 197, 211, 61], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "associated_quote_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "buyback_fee_recipient", + "writable": true + }, + { + "name": "associated_quote_buyback_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "buyback_fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "associated_base_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "associated_quote_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "associated_base_user", + "writable": true + }, + { + "name": "associated_quote_user", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "associated_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "sharing_config", + "docs": [ + "seeds; the account is intentionally not deserialized here because it may be uninitialized", + "for mints that have not created a fee sharing config. Handlers must check", + "`data_is_empty()` / owner before reading." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "associated_user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] + }, + { + "kind": "const", + "value": [ + 1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, + 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, + 182, 253, 109, 24, 176 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "max_sol_cost", + "type": "u64" + } + ] + }, + { + "name": "claim_cashback", + "discriminator": [37, 58, 35, 126, 190, 53, 228, 197], + "accounts": [ + { + "name": "user", + "writable": true + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [] + }, + { + "name": "claim_cashback_v2", + "discriminator": [122, 243, 204, 65, 94, 116, 29, 55], + "accounts": [ + { + "name": "user", + "writable": true + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "quote_mint" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "associated_user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "associated_quote_user", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [] + }, + { + "name": "claim_token_incentives", + "discriminator": [16, 4, 71, 28, 204, 1, 40, 27], + "accounts": [ + { + "name": "user" + }, + { + "name": "user_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + } + ] + } + }, + { + "name": "global_incentive_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "global_volume_accumulator" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "mint", + "relations": ["global_volume_accumulator"] + }, + { + "name": "token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "payer", + "writable": true, + "signer": true + } + ], + "args": [] + }, + { + "name": "close_user_volume_accumulator", + "discriminator": [249, 69, 164, 218, 150, 103, 84, 138], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "collect_creator_fee", + "docs": [ + "Collects creator_fee from creator_vault to the coin creator account" + ], + "discriminator": [20, 22, 86, 123, 198, 28, 219, 132], + "accounts": [ + { + "name": "creator", + "writable": true + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "creator" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "collect_creator_fee_v2", + "docs": [ + "Collects creator_fee from creator_vault to the coin creator account" + ], + "discriminator": [207, 17, 138, 242, 4, 34, 19, 56], + "accounts": [ + { + "name": "creator", + "writable": true + }, + { + "name": "creator_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "creator" + } + ] + } + }, + { + "name": "creator_vault_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "quote_mint" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "create", + "docs": ["Creates a new coin and bonding curve."], + "discriminator": [24, 30, 200, 40, 5, 28, 7, 119], + "accounts": [ + { + "name": "mint", + "writable": true, + "signer": true + }, + { + "name": "mint_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, 105, 110, 116, 45, 97, 117, 116, 104, 111, 114, 105, 116, + 121 + ] + } + ] + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "associated_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "const", + "value": [ + 6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, + 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, + 245, 133, 126, 255, 0, 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "mpl_token_metadata", + "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + }, + { + "name": "metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + }, + { + "kind": "const", + "value": [ + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, + 4, 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, + 209, 188, 3, 248, 41, 70 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "account", + "path": "mpl_token_metadata" + } + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "creator", + "type": "pubkey" + } + ] + }, + { + "name": "create_v2", + "docs": ["Creates a new spl-22 coin and bonding curve."], + "discriminator": [214, 144, 76, 236, 95, 139, 49, 180], + "accounts": [ + { + "name": "mint", + "writable": true, + "signer": true + }, + { + "name": "mint_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, 105, 110, 116, 45, 97, 117, 116, 104, 111, 114, 105, 116, + 121 + ] + } + ] + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "associated_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "mayhem_program_id", + "writable": true, + "address": "MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e" + }, + { + "name": "global_params", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 45, 112, 97, 114, 97, 109, 115 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 5, 42, 229, 215, 167, 218, 167, 36, 166, 234, 176, 167, 41, 84, + 145, 133, 90, 212, 160, 103, 22, 96, 103, 76, 78, 3, 69, 89, + 128, 61, 101, 163 + ] + } + } + }, + { + "name": "sol_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 111, 108, 45, 118, 97, 117, 108, 116] + } + ], + "program": { + "kind": "const", + "value": [ + 5, 42, 229, 215, 167, 218, 167, 36, 166, 234, 176, 167, 41, 84, + 145, 133, 90, 212, 160, 103, 22, 96, 103, 76, 78, 3, 69, 89, + 128, 61, 101, 163 + ] + } + } + }, + { + "name": "mayhem_state", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, 97, 121, 104, 101, 109, 45, 115, 116, 97, 116, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 5, 42, 229, 215, 167, 218, 167, 36, 166, 234, 176, 167, 41, 84, + 145, 133, 90, 212, 160, 103, 22, 96, 103, 76, 78, 3, 69, 89, + 128, 61, 101, 163 + ] + } + } + }, + { + "name": "mayhem_token_vault", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" + }, + { + "name": "is_cashback_enabled", + "type": { + "defined": { + "name": "OptionBool" + } + } + } + ] + }, + { + "name": "distribute_creator_fees", + "docs": [ + "Distributes creator fees to shareholders based on their share percentages", + "The creator vault needs to have at least the minimum distributable amount to distribute fees", + "This can be checked with the get_minimum_distributable_fee instruction" + ], + "discriminator": [165, 114, 103, 0, 121, 206, 247, 81], + "accounts": [ + { + "name": "mint", + "relations": ["sharing_config"] + }, + { + "name": "bonding_curve", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "sharing_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [], + "returns": { + "defined": { + "name": "DistributeCreatorFeesEvent" + } + } + }, + { + "name": "distribute_creator_fees_v2", + "discriminator": [255, 203, 19, 79, 244, 68, 8, 159], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "mint", + "relations": ["sharing_config"] + }, + { + "name": "bonding_curve", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "sharing_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "creator_vault_quote_token_account", + "docs": [ + "Deserialized manually in the handler for non-legacy quote mints." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "quote_mint" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [ + { + "name": "initialize_ata", + "type": "bool" + } + ], + "returns": { + "defined": { + "name": "DistributeCreatorFeesEvent" + } + } + }, + { + "name": "extend_account", + "docs": ["Extends the size of program-owned accounts"], + "discriminator": [234, 102, 194, 203, 150, 72, 62, 229], + "accounts": [ + { + "name": "account", + "writable": true + }, + { + "name": "user", + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "get_minimum_distributable_fee", + "docs": [ + "Permissionless instruction to check the minimum required fees for distribution", + "Returns the minimum required balance from the creator_vault and whether distribution can proceed" + ], + "discriminator": [117, 225, 127, 202, 134, 95, 68, 35], + "accounts": [ + { + "name": "mint", + "relations": ["sharing_config"] + }, + { + "name": "bonding_curve", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "sharing_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "creator_vault", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + } + ], + "args": [], + "returns": { + "defined": { + "name": "MinimumDistributableFeeEvent" + } + } + }, + { + "name": "init_user_volume_accumulator", + "discriminator": [94, 6, 202, 115, 255, 96, 232, 183], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "user" + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "initialize", + "docs": ["Creates the global state."], + "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "migrate", + "docs": [ + "Migrates liquidity to pump_amm if the bonding curve is complete" + ], + "discriminator": [155, 234, 231, 146, 236, 158, 162, 30], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "withdraw_authority", + "writable": true, + "relations": ["global"] + }, + { + "name": "mint" + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "associated_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "mint" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "user", + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "pump_amm", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "pool", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 111, 108] + }, + { + "kind": "const", + "value": [0, 0] + }, + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "mint" + }, + { + "kind": "account", + "path": "wsol_mint" + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "pool_authority", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, 111, 111, 108, 45, 97, 117, 116, 104, 111, 114, 105, 116, + 121 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "pool_authority_mint_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "mint" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_authority_wsol_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "wsol_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "amm_global_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 + ] + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "wsol_mint", + "address": "So11111111111111111111111111111111111111112" + }, + { + "name": "lp_mint", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, 111, 111, 108, 95, 108, 112, 95, 109, 105, 110, 116 + ] + }, + { + "kind": "account", + "path": "pool" + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "user_pool_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "token_2022_program" + }, + { + "kind": "account", + "path": "lp_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_base_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "mint" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_quote_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "wsol_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "token_2022_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "pump_amm_event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "migrate_bonding_curve_creator", + "discriminator": [87, 124, 52, 191, 52, 38, 214, 232], + "accounts": [ + { + "name": "mint", + "relations": ["sharing_config"] + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "sharing_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "migrate_v2", + "docs": [ + "Migrates liquidity to pump_amm if the bonding curve is complete" + ], + "discriminator": [187, 203, 18, 31, 206, 237, 254, 41], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "withdraw_authority", + "writable": true, + "relations": ["global"] + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "associated_base_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "associated_quote_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "user", + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "pump_amm", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "pool", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 111, 108] + }, + { + "kind": "const", + "value": [0, 0] + }, + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "pool_authority", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, 111, 111, 108, 45, 97, 117, 116, 104, 111, 114, 105, 116, + 121 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "pool_authority_mint_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_authority_quote_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "amm_global_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 + ] + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "lp_mint", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, 111, 111, 108, 95, 108, 112, 95, 109, 105, 110, 116 + ] + }, + { + "kind": "account", + "path": "pool" + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "user_pool_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "token_2022_program" + }, + { + "kind": "account", + "path": "lp_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_base_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_quote_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "token_2022_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "pump_amm_event_authority" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "remove_quote_mint", + "discriminator": [177, 65, 223, 38, 88, 209, 158, 155], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "quote_mint", + "type": "pubkey" + } + ] + }, + { + "name": "sell", + "docs": [ + "Sells tokens into a bonding curve.", + "For cashback coins, pass as remaining_accounts: [0] user_volume_accumulator,", + "[1] bonding_curve_v2. If provided and valid, creator_fee goes to user_volume_accumulator.", + "Otherwise, falls back to transferring creator_fee to creator_vault." + ], + "discriminator": [51, 230, 133, 164, 1, 127, 131, 173], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "mint" + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "associated_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "associated_user", + "writable": true + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "token_program" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] + }, + { + "kind": "const", + "value": [ + 1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, + 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, + 182, 253, 109, 24, 176 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "min_sol_output", + "type": "u64" + } + ] + }, + { + "name": "sell_v2", + "discriminator": [93, 246, 130, 60, 231, 233, 64, 178], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "base_mint" + }, + { + "name": "quote_mint" + }, + { + "name": "base_token_program" + }, + { + "name": "quote_token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "fee_recipient", + "writable": true + }, + { + "name": "associated_quote_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "buyback_fee_recipient", + "writable": true + }, + { + "name": "associated_quote_buyback_fee_recipient", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "buyback_fee_recipient" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "associated_base_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "associated_quote_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "associated_base_user", + "writable": true + }, + { + "name": "associated_quote_user", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116 + ] + }, + { + "kind": "account", + "path": "bonding_curve.creator", + "account": "BondingCurve" + } + ] + } + }, + { + "name": "associated_creator_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator_vault" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "sharing_config", + "docs": [ + "seeds; the account is intentionally not deserialized here because it may be uninitialized", + "for mints that have not created a fee sharing config. Handlers must check", + "`data_is_empty()` / owner before reading." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105, + 103 + ] + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21, + 39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20, + 194, 124, 112 + ] + } + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "associated_user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "user_volume_accumulator" + }, + { + "kind": "account", + "path": "quote_token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [102, 101, 101, 95, 99, 111, 110, 102, 105, 103] + }, + { + "kind": "const", + "value": [ + 1, 86, 224, 246, 147, 102, 90, 207, 68, 219, 21, 104, 191, 23, + 91, 170, 81, 137, 203, 151, 245, 210, 255, 59, 101, 93, 43, + 182, 253, 109, 24, 176 + ] + } + ], + "program": { + "kind": "account", + "path": "fee_program" + } + } + }, + { + "name": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "min_sol_output", + "type": "u64" + } + ] + }, + { + "name": "set_creator", + "docs": [ + "Allows Global::set_creator_authority to set the bonding curve creator from Metaplex metadata or input argument" + ], + "discriminator": [254, 148, 255, 112, 207, 142, 170, 165], + "accounts": [ + { + "name": "set_creator_authority", + "signer": true, + "relations": ["global"] + }, + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "mint" + }, + { + "name": "metadata", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + }, + { + "kind": "const", + "value": [ + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, + 4, 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, + 209, 188, 3, 248, 41, 70 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, 4, + 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, 209, + 188, 3, 248, 41, 70 + ] + } + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "creator", + "type": "pubkey" + } + ] + }, + { + "name": "set_mayhem_virtual_params", + "discriminator": [61, 169, 188, 191, 153, 149, 42, 97], + "accounts": [ + { + "name": "sol_vault_authority", + "writable": true, + "signer": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 111, 108, 45, 118, 97, 117, 108, 116] + } + ], + "program": { + "kind": "const", + "value": [ + 5, 42, 229, 215, 167, 218, 167, 36, 166, 234, 176, 167, 41, 84, + 145, 133, 90, 212, 160, 103, 22, 96, 103, 76, 78, 3, 69, 89, + 128, 61, 101, 163 + ] + } + } + }, + { + "name": "mayhem_token_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "sol_vault_authority" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, + 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, + 219, 233, 248, 89 + ] + } + } + }, + { + "name": "mint" + }, + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "token_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "set_metaplex_creator", + "docs": [ + "Syncs the bonding curve creator with the Metaplex metadata creator if it exists" + ], + "discriminator": [138, 96, 174, 217, 48, 85, 197, 246], + "accounts": [ + { + "name": "mint" + }, + { + "name": "metadata", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + }, + { + "kind": "const", + "value": [ + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, + 4, 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, + 209, 188, 3, 248, 41, 70 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, 4, + 195, 205, 88, 184, 108, 115, 26, 160, 253, 181, 73, 182, 209, + 188, 3, 248, 41, 70 + ] + } + } + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "set_params", + "docs": ["Sets the global state parameters."], + "discriminator": [27, 234, 178, 52, 147, 2, 187, 141], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "initial_virtual_token_reserves", + "type": "u64" + }, + { + "name": "initial_virtual_sol_reserves", + "type": "u64" + }, + { + "name": "initial_real_token_reserves", + "type": "u64" + }, + { + "name": "token_total_supply", + "type": "u64" + }, + { + "name": "fee_basis_points", + "type": "u64" + }, + { + "name": "withdraw_authority", + "type": "pubkey" + }, + { + "name": "enable_migrate", + "type": "bool" + }, + { + "name": "pool_migration_fee", + "type": "u64" + }, + { + "name": "creator_fee_basis_points", + "type": "u64" + }, + { + "name": "set_creator_authority", + "type": "pubkey" + }, + { + "name": "admin_set_creator_authority", + "type": "pubkey" + } + ] + }, + { + "name": "set_reserved_fee_recipients", + "discriminator": [111, 172, 162, 232, 114, 89, 213, 142], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "whitelist_pda", + "type": "pubkey" + } + ] + }, + { + "name": "set_virtual_quote_reserves", + "discriminator": [101, 135, 191, 104, 9, 88, 20, 96], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "initial_virtual_quote_reserves", + "type": "u64" + } + ] + }, + { + "name": "sync_user_volume_accumulator", + "discriminator": [86, 31, 192, 87, 163, 87, 79, 238], + "accounts": [ + { + "name": "user" + }, + { + "name": "global_volume_accumulator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, 108, 111, 98, 97, 108, 95, 118, 111, 108, 117, 109, 101, + 95, 97, 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + } + ] + } + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, 115, 101, 114, 95, 118, 111, 108, 117, 109, 101, 95, 97, + 99, 99, 117, 109, 117, 108, 97, 116, 111, 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "toggle_cashback_enabled", + "discriminator": [115, 103, 224, 255, 189, 89, 86, 195], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "enabled", + "type": "bool" + } + ] + }, + { + "name": "toggle_create_v2", + "discriminator": [28, 255, 230, 240, 172, 107, 203, 171], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "enabled", + "type": "bool" + } + ] + }, + { + "name": "toggle_mayhem_mode", + "discriminator": [1, 9, 111, 208, 100, 31, 255, 163], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "enabled", + "type": "bool" + } + ] + }, + { + "name": "update_buyback_config", + "discriminator": [251, 224, 171, 146, 160, 26, 113, 233], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": ["global"] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "buyback_basis_points", + "type": { + "option": "u64" + } + } + ] + }, + { + "name": "update_global_authority", + "discriminator": [227, 181, 74, 196, 208, 21, 97, 213], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [103, 108, 111, 98, 97, 108] + } + ] + } + }, + { + "name": "authority", + "signer": true, + "relations": ["global"] + }, + { + "name": "new_authority" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111, + 114, 105, 116, 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "BondingCurve", + "discriminator": [23, 183, 248, 55, 96, 216, 172, 96] + }, + { + "name": "FeeConfig", + "discriminator": [143, 52, 146, 187, 219, 123, 76, 155] + }, + { + "name": "Global", + "discriminator": [167, 232, 232, 177, 200, 108, 114, 127] + }, + { + "name": "GlobalVolumeAccumulator", + "discriminator": [202, 42, 246, 43, 142, 190, 30, 255] + }, + { + "name": "SharingConfig", + "discriminator": [216, 74, 9, 0, 56, 140, 93, 75] + }, + { + "name": "UserVolumeAccumulator", + "discriminator": [86, 255, 112, 14, 102, 53, 154, 250] + } + ], + "events": [ + { + "name": "AdminSetCreatorEvent", + "discriminator": [64, 69, 192, 104, 29, 30, 25, 107] + }, + { + "name": "AdminSetIdlAuthorityEvent", + "discriminator": [245, 59, 70, 34, 75, 185, 109, 92] + }, + { + "name": "AdminUpdateTokenIncentivesEvent", + "discriminator": [147, 250, 108, 120, 247, 29, 67, 222] + }, + { + "name": "ClaimCashbackEvent", + "discriminator": [226, 214, 246, 33, 7, 242, 147, 229] + }, + { + "name": "ClaimTokenIncentivesEvent", + "discriminator": [79, 172, 246, 49, 205, 91, 206, 232] + }, + { + "name": "CloseUserVolumeAccumulatorEvent", + "discriminator": [146, 159, 189, 172, 146, 88, 56, 244] + }, + { + "name": "CollectCreatorFeeEvent", + "discriminator": [122, 2, 127, 1, 14, 191, 12, 175] + }, + { + "name": "CompleteEvent", + "discriminator": [95, 114, 97, 156, 212, 46, 152, 8] + }, + { + "name": "CompletePumpAmmMigrationEvent", + "discriminator": [189, 233, 93, 185, 92, 148, 234, 148] + }, + { + "name": "CreateEvent", + "discriminator": [27, 114, 169, 77, 222, 235, 99, 118] + }, + { + "name": "DistributeCreatorFeesEvent", + "discriminator": [165, 55, 129, 112, 4, 179, 202, 40] + }, + { + "name": "ExtendAccountEvent", + "discriminator": [97, 97, 215, 144, 93, 146, 22, 124] + }, + { + "name": "InitUserVolumeAccumulatorEvent", + "discriminator": [134, 36, 13, 72, 232, 101, 130, 216] + }, + { + "name": "MigrateBondingCurveCreatorEvent", + "discriminator": [155, 167, 104, 220, 213, 108, 243, 3] + }, + { + "name": "MinimumDistributableFeeEvent", + "discriminator": [168, 216, 132, 239, 235, 182, 49, 52] + }, + { + "name": "ReservedFeeRecipientsEvent", + "discriminator": [43, 188, 250, 18, 221, 75, 187, 95] + }, + { + "name": "SetCreatorEvent", + "discriminator": [237, 52, 123, 37, 245, 251, 72, 210] + }, + { + "name": "SetMetaplexCreatorEvent", + "discriminator": [142, 203, 6, 32, 127, 105, 191, 162] + }, + { + "name": "SetParamsEvent", + "discriminator": [223, 195, 159, 246, 62, 48, 143, 131] + }, + { + "name": "SyncUserVolumeAccumulatorEvent", + "discriminator": [197, 122, 167, 124, 116, 81, 91, 255] + }, + { + "name": "TradeEvent", + "discriminator": [189, 219, 127, 211, 78, 230, 97, 238] + }, + { + "name": "UpdateGlobalAuthorityEvent", + "discriminator": [182, 195, 137, 42, 35, 206, 207, 247] + }, + { + "name": "UpdateMayhemVirtualParamsEvent", + "discriminator": [117, 123, 228, 182, 161, 168, 220, 214] + } + ], + "errors": [ + { + "code": 6000, + "name": "NotAuthorized", + "msg": "The given account is not authorized to execute this instruction." + }, + { + "code": 6001, + "name": "AlreadyInitialized", + "msg": "The program is already initialized." + }, + { + "code": 6002, + "name": "TooMuchSolRequired", + "msg": "slippage: Too much SOL required to buy the given amount of tokens." + }, + { + "code": 6003, + "name": "TooLittleSolReceived", + "msg": "slippage: Too little SOL received to sell the given amount of tokens." + }, + { + "code": 6004, + "name": "MintDoesNotMatchBondingCurve", + "msg": "The mint does not match the bonding curve." + }, + { + "code": 6005, + "name": "BondingCurveComplete", + "msg": "The bonding curve has completed and liquidity migrated to raydium." + }, + { + "code": 6006, + "name": "BondingCurveNotComplete", + "msg": "The bonding curve has not completed." + }, + { + "code": 6007, + "name": "NotInitialized", + "msg": "The program is not initialized." + }, + { + "code": 6008, + "name": "WithdrawTooFrequent", + "msg": "Withdraw too frequent" + }, + { + "code": 6009, + "name": "NewSizeShouldBeGreaterThanCurrentSize", + "msg": "new_size should be > current_size" + }, + { + "code": 6010, + "name": "AccountTypeNotSupported", + "msg": "Account type not supported" + }, + { + "code": 6011, + "name": "InitialRealTokenReservesShouldBeLessThanTokenTotalSupply", + "msg": "initial_real_token_reserves should be less than token_total_supply" + }, + { + "code": 6012, + "name": "InitialVirtualTokenReservesShouldBeGreaterThanInitialRealTokenReserves", + "msg": "initial_virtual_token_reserves should be greater than initial_real_token_reserves" + }, + { + "code": 6013, + "name": "FeeBasisPointsGreaterThanMaximum", + "msg": "fee_basis_points greater than maximum" + }, + { + "code": 6014, + "name": "AllZerosWithdrawAuthority", + "msg": "Withdraw authority cannot be set to System Program ID" + }, + { + "code": 6015, + "name": "PoolMigrationFeeShouldBeLessThanFinalRealSolReserves", + "msg": "pool_migration_fee should be less than final_real_sol_reserves" + }, + { + "code": 6016, + "name": "PoolMigrationFeeShouldBeGreaterThanCreatorFeePlusMaxMigrateFees", + "msg": "pool_migration_fee should be greater than creator_fee + MAX_MIGRATE_FEES" + }, + { + "code": 6017, + "name": "DisabledWithdraw", + "msg": "Migrate instruction is disabled" + }, + { + "code": 6018, + "name": "DisabledMigrate", + "msg": "Migrate instruction is disabled" + }, + { + "code": 6019, + "name": "InvalidCreator", + "msg": "Invalid creator pubkey" + }, + { + "code": 6020, + "name": "BuyZeroAmount", + "msg": "Buy zero amount" + }, + { + "code": 6021, + "name": "NotEnoughTokensToBuy", + "msg": "Not enough tokens to buy" + }, + { + "code": 6022, + "name": "SellZeroAmount", + "msg": "Sell zero amount" + }, + { + "code": 6023, + "name": "NotEnoughTokensToSell", + "msg": "Not enough tokens to sell" + }, + { + "code": 6024, + "name": "Overflow", + "msg": "Overflow" + }, + { + "code": 6025, + "name": "Truncation", + "msg": "Truncation" + }, + { + "code": 6026, + "name": "DivisionByZero", + "msg": "Division by zero" + }, + { + "code": 6027, + "name": "NotEnoughRemainingAccounts", + "msg": "Not enough remaining accounts" + }, + { + "code": 6028, + "name": "AllFeeRecipientsShouldBeNonZero", + "msg": "All fee recipients should be non-zero" + }, + { + "code": 6029, + "name": "UnsortedNotUniqueFeeRecipients", + "msg": "Unsorted or not unique fee recipients" + }, + { + "code": 6030, + "name": "CreatorShouldNotBeZero", + "msg": "Creator should not be zero" + }, + { + "code": 6031, + "name": "StartTimeInThePast" + }, + { + "code": 6032, + "name": "EndTimeInThePast" + }, + { + "code": 6033, + "name": "EndTimeBeforeStartTime" + }, + { + "code": 6034, + "name": "TimeRangeTooLarge" + }, + { + "code": 6035, + "name": "EndTimeBeforeCurrentDay" + }, + { + "code": 6036, + "name": "SupplyUpdateForFinishedRange" + }, + { + "code": 6037, + "name": "DayIndexAfterEndIndex" + }, + { + "code": 6038, + "name": "DayInActiveRange" + }, + { + "code": 6039, + "name": "InvalidIncentiveMint" + }, + { + "code": 6040, + "name": "BuyNotEnoughSolToCoverRent", + "msg": "Buy: Not enough SOL to cover for rent exemption." + }, + { + "code": 6041, + "name": "BuyNotEnoughSolToCoverFees", + "msg": "Buy: Not enough SOL to cover for fees." + }, + { + "code": 6042, + "name": "BuySlippageBelowMinTokensOut", + "msg": "Slippage: Would buy less tokens than expected min_tokens_out" + }, + { + "code": 6043, + "name": "NameTooLong" + }, + { + "code": 6044, + "name": "SymbolTooLong" + }, + { + "code": 6045, + "name": "UriTooLong" + }, + { + "code": 6046, + "name": "CreateV2Disabled" + }, + { + "code": 6047, + "name": "CpitializeMayhemFailed" + }, + { + "code": 6048, + "name": "MayhemModeDisabled" + }, + { + "code": 6049, + "name": "CreatorMigratedToSharingConfig", + "msg": "creator has been migrated to sharing config, use pump_fees::reset_fee_sharing_config instead" + }, + { + "code": 6050, + "name": "UnableToDistributeCreatorVaultMigratedToSharingConfig", + "msg": "creator_vault has been migrated to sharing config, use pump:distribute_creator_fees instead" + }, + { + "code": 6051, + "name": "SharingConfigNotActive", + "msg": "Sharing config is not active" + }, + { + "code": 6052, + "name": "UnableToDistributeCreatorFeesToExecutableRecipient", + "msg": "The recipient account is executable, so it cannot receive lamports, remove it from the team first" + }, + { + "code": 6053, + "name": "BondingCurveAndSharingConfigCreatorMismatch", + "msg": "Bonding curve creator does not match sharing config" + }, + { + "code": 6054, + "name": "ShareholdersAndRemainingAccountsMismatch", + "msg": "Remaining accounts do not match shareholders, make sure to pass exactly the same pubkeys in the same order" + }, + { + "code": 6055, + "name": "InvalidShareBps", + "msg": "Share bps must be greater than 0" + }, + { + "code": 6056, + "name": "CashbackNotEnabled", + "msg": "Cashback is not enabled" + }, + { + "code": 6057, + "name": "BuybackFeeRecipientNotAuthorized", + "msg": "Buyback fee recipient not authorized" + }, + { + "code": 6058, + "name": "AllBuybackFeeRecipientsShouldBeNonZero" + }, + { + "code": 6059, + "name": "NotUniqueBuybackFeeRecipients" + }, + { + "code": 6060, + "name": "BuybackBasisPointsOutOfRange", + "msg": "buyback_basis_points must be <= 10_000" + }, + { + "code": 6061, + "name": "WrongBuybackFeeRecipientsCount", + "msg": "buyback fee recipients require exactly 8 remaining accounts (or none)" + }, + { + "code": 6062, + "name": "BuybackFeeRecipientMissing" + }, + { + "code": 6063, + "name": "UnsupportedQuoteMint", + "msg": "Unsupported quote mint" + }, + { + "code": 6064, + "name": "InvalidQuoteTokenProgram", + "msg": "Create v2: quote token program must be legacy SPL Token" + }, + { + "code": 6065, + "name": "InvalidAssociatedQuoteBondingCurve", + "msg": "Create v2: associated quote bonding curve address does not match derivation" + }, + { + "code": 6066, + "name": "QuoteMintWhitelistFull", + "msg": "Quote mint whitelist is full" + }, + { + "code": 6067, + "name": "QuoteMintAlreadyWhitelisted", + "msg": "Quote mint is already whitelisted" + }, + { + "code": 6068, + "name": "QuoteMintNotWhitelisted", + "msg": "Quote mint is not in the whitelist" + }, + { + "code": 6069, + "name": "QuoteMintNotEligibleForWhitelist", + "msg": "Quote mint cannot be added or removed via whitelist (default or native SOL mint)" + }, + { + "code": 6070, + "name": "UnableToDistributeCreatorFeesToUninitializedAccount", + "msg": "Unable to distribute creator fees to uninitialized account" + }, + { + "code": 6071, + "name": "MayhemModeQuoteMintNotAllowed", + "msg": "Mayhem mode quote mint not allowed" + } + ], + "types": [ + { + "name": "AdminSetCreatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "admin_set_creator_authority", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "old_creator", + "type": "pubkey" + }, + { + "name": "new_creator", + "type": "pubkey" + } + ] + } + }, + { + "name": "AdminSetIdlAuthorityEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "idl_authority", + "type": "pubkey" + } + ] + } + }, + { + "name": "AdminUpdateTokenIncentivesEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "start_time", + "type": "i64" + }, + { + "name": "end_time", + "type": "i64" + }, + { + "name": "day_number", + "type": "u64" + }, + { + "name": "token_supply_per_day", + "type": "u64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "seconds_in_a_day", + "type": "i64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "BondingCurve", + "type": { + "kind": "struct", + "fields": [ + { + "name": "virtual_token_reserves", + "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "u64" + }, + { + "name": "real_token_reserves", + "type": "u64" + }, + { + "name": "real_quote_reserves", + "type": "u64" + }, + { + "name": "token_total_supply", + "type": "u64" + }, + { + "name": "complete", + "type": "bool" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" + }, + { + "name": "is_cashback_coin", + "type": "bool" + }, + { + "name": "quote_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "ClaimCashbackEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "total_claimed", + "type": "u64" + }, + { + "name": "total_cashback_earned", + "type": "u64" + } + ] + } + }, + { + "name": "ClaimTokenIncentivesEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "total_claimed_tokens", + "type": "u64" + }, + { + "name": "current_sol_volume", + "type": "u64" + } + ] + } + }, + { + "name": "CloseUserVolumeAccumulatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "total_unclaimed_tokens", + "type": "u64" + }, + { + "name": "total_claimed_tokens", + "type": "u64" + }, + { + "name": "current_sol_volume", + "type": "u64" + }, + { + "name": "last_update_timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "CollectCreatorFeeEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "creator_fee", + "type": "u64" + }, + { + "name": "quote_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "CompleteEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "quote_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "CompletePumpAmmMigrationEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "mint_amount", + "type": "u64" + }, + { + "name": "sol_amount", + "type": "u64" + }, + { + "name": "pool_migration_fee", + "type": "u64" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "quote_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "ConfigStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Paused" + }, + { + "name": "Active" + } + ] + } + }, + { + "name": "CreateEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "virtual_token_reserves", + "type": "u64" + }, + { + "name": "virtual_sol_reserves", + "type": "u64" + }, + { + "name": "real_token_reserves", + "type": "u64" + }, + { + "name": "token_total_supply", + "type": "u64" + }, + { + "name": "token_program", + "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" + }, + { + "name": "is_cashback_enabled", + "type": "bool" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "virtual_quote_reserves", + "type": "u64" + } + ] + } + }, + { + "name": "DistributeCreatorFeesEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "sharing_config", + "type": "pubkey" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "shareholders", + "type": { + "vec": { + "defined": { + "name": "Shareholder" + } + } + } + }, + { + "name": "distributed", + "type": "u64" + }, + { + "name": "quote_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "ExtendAccountEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "account", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "current_size", + "type": "u64" + }, + { + "name": "new_size", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "FeeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "flat_fees", + "type": { + "defined": { + "name": "Fees" + } + } + }, + { + "name": "fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } + }, + { + "name": "stable_fee_tiers", + "type": { + "vec": { + "defined": { + "name": "FeeTier" + } + } + } + } + ] + } + }, + { + "name": "FeeTier", + "type": { + "kind": "struct", + "fields": [ + { + "name": "market_cap_lamports_threshold", + "type": "u128" + }, + { + "name": "fees", + "type": { + "defined": { + "name": "Fees" + } + } + } + ] + } + }, + { + "name": "Fees", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lp_fee_bps", + "type": "u64" + }, + { + "name": "protocol_fee_bps", + "type": "u64" + }, + { + "name": "creator_fee_bps", + "type": "u64" + } + ] + } + }, + { + "name": "Global", + "type": { + "kind": "struct", + "fields": [ + { + "name": "initialized", + "docs": ["Unused"], + "type": "bool" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "fee_recipient", + "type": "pubkey" + }, + { + "name": "initial_virtual_token_reserves", + "type": "u64" + }, + { + "name": "initial_virtual_sol_reserves", + "type": "u64" + }, + { + "name": "initial_real_token_reserves", + "type": "u64" + }, + { + "name": "token_total_supply", + "type": "u64" + }, + { + "name": "fee_basis_points", + "type": "u64" + }, + { + "name": "withdraw_authority", + "type": "pubkey" + }, + { + "name": "enable_migrate", + "docs": ["Unused"], + "type": "bool" + }, + { + "name": "pool_migration_fee", + "type": "u64" + }, + { + "name": "creator_fee_basis_points", + "type": "u64" + }, + { + "name": "fee_recipients", + "type": { + "array": ["pubkey", 7] + } + }, + { + "name": "set_creator_authority", + "type": "pubkey" + }, + { + "name": "admin_set_creator_authority", + "type": "pubkey" + }, + { + "name": "create_v2_enabled", + "type": "bool" + }, + { + "name": "whitelist_pda", + "type": "pubkey" + }, + { + "name": "reserved_fee_recipient", + "type": "pubkey" + }, + { + "name": "mayhem_mode_enabled", + "type": "bool" + }, + { + "name": "reserved_fee_recipients", + "type": { + "array": ["pubkey", 7] + } + }, + { + "name": "is_cashback_enabled", + "type": "bool" + }, + { + "name": "buyback_fee_recipients", + "type": { + "array": ["pubkey", 8] + } + }, + { + "name": "buyback_basis_points", + "type": "u64" + }, + { + "name": "initial_virtual_quote_reserves", + "type": "u64" + }, + { + "name": "whitelisted_quote_mints", + "type": { + "array": ["pubkey", 1] + } + } + ] + } + }, + { + "name": "GlobalVolumeAccumulator", + "type": { + "kind": "struct", + "fields": [ + { + "name": "start_time", + "type": "i64" + }, + { + "name": "end_time", + "type": "i64" + }, + { + "name": "seconds_in_a_day", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "total_token_supply", + "type": { + "array": ["u64", 30] + } + }, + { + "name": "sol_volumes", + "type": { + "array": ["u64", 30] + } + } + ] + } + }, + { + "name": "InitUserVolumeAccumulatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "payer", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "MigrateBondingCurveCreatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "sharing_config", + "type": "pubkey" + }, + { + "name": "old_creator", + "type": "pubkey" + }, + { + "name": "new_creator", + "type": "pubkey" + } + ] + } + }, + { + "name": "MinimumDistributableFeeEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "minimum_required", + "type": "u64" + }, + { + "name": "distributable_fees", + "type": "u64" + }, + { + "name": "can_distribute", + "type": "bool" + } + ] + } + }, + { + "name": "OptionBool", + "type": { + "kind": "struct", + "fields": ["bool"] + } + }, + { + "name": "ReservedFeeRecipientsEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "reserved_fee_recipient", + "type": "pubkey" + }, + { + "name": "reserved_fee_recipients", + "type": { + "array": ["pubkey", 7] + } + } + ] + } + }, + { + "name": "SetCreatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + } + ] + } + }, + { + "name": "SetMetaplexCreatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "bonding_curve", + "type": "pubkey" + }, + { + "name": "metadata", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + } + ] + } + }, + { + "name": "SetParamsEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "initial_virtual_token_reserves", + "type": "u64" + }, + { + "name": "initial_virtual_sol_reserves", + "type": "u64" + }, + { + "name": "initial_real_token_reserves", + "type": "u64" + }, + { + "name": "final_real_sol_reserves", + "type": "u64" + }, + { + "name": "token_total_supply", + "type": "u64" + }, + { + "name": "fee_basis_points", + "type": "u64" + }, + { + "name": "withdraw_authority", + "type": "pubkey" + }, + { + "name": "enable_migrate", + "type": "bool" + }, + { + "name": "pool_migration_fee", + "type": "u64" + }, + { + "name": "creator_fee_basis_points", + "type": "u64" + }, + { + "name": "fee_recipients", + "type": { + "array": ["pubkey", 8] + } + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "set_creator_authority", + "type": "pubkey" + }, + { + "name": "admin_set_creator_authority", + "type": "pubkey" + } + ] + } + }, + { + "name": "Shareholder", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": "pubkey" + }, + { + "name": "share_bps", + "type": "u16" + } + ] + } + }, + { + "name": "SharingConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "version", + "type": "u8" + }, + { + "name": "status", + "type": { + "defined": { + "name": "ConfigStatus" + } + } + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "admin_revoked", + "type": "bool" + }, + { + "name": "shareholders", + "type": { + "vec": { + "defined": { + "name": "Shareholder" + } + } + } + } + ] + } + }, + { + "name": "SyncUserVolumeAccumulatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "total_claimed_tokens_before", + "type": "u64" + }, + { + "name": "total_claimed_tokens_after", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "TradeEvent", + "docs": ["ix_name: \"buy\" | \"sell\" | \"buy_exact_sol_in\""], + "type": { + "kind": "struct", + "fields": [ + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "sol_amount", + "type": "u64" + }, + { + "name": "token_amount", + "type": "u64" + }, + { + "name": "is_buy", + "type": "bool" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "virtual_sol_reserves", + "type": "u64" + }, + { + "name": "virtual_token_reserves", + "type": "u64" + }, + { + "name": "real_sol_reserves", + "type": "u64" + }, + { + "name": "real_token_reserves", + "type": "u64" + }, + { + "name": "fee_recipient", + "type": "pubkey" + }, + { + "name": "fee_basis_points", + "type": "u64" + }, + { + "name": "fee", + "type": "u64" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "creator_fee_basis_points", + "type": "u64" + }, + { + "name": "creator_fee", + "type": "u64" + }, + { + "name": "track_volume", + "type": "bool" + }, + { + "name": "total_unclaimed_tokens", + "type": "u64" + }, + { + "name": "total_claimed_tokens", + "type": "u64" + }, + { + "name": "current_sol_volume", + "type": "u64" + }, + { + "name": "last_update_timestamp", + "type": "i64" + }, + { + "name": "ix_name", + "type": "string" + }, + { + "name": "mayhem_mode", + "type": "bool" + }, + { + "name": "cashback_fee_basis_points", + "type": "u64" + }, + { + "name": "cashback", + "type": "u64" + }, + { + "name": "buyback_fee_basis_points", + "type": "u64" + }, + { + "name": "buyback_fee", + "type": "u64" + }, + { + "name": "shareholders", + "type": { + "vec": { + "defined": { + "name": "Shareholder" + } + } + } + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "quote_amount", + "type": "u64" + }, + { + "name": "virtual_quote_reserves", + "type": "u64" + }, + { + "name": "real_quote_reserves", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateGlobalAuthorityEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "global", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "new_authority", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "UpdateMayhemVirtualParamsEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "virtual_token_reserves", + "type": "u64" + }, + { + "name": "virtual_sol_reserves", + "type": "u64" + }, + { + "name": "new_virtual_token_reserves", + "type": "u64" + }, + { + "name": "new_virtual_sol_reserves", + "type": "u64" + }, + { + "name": "real_token_reserves", + "type": "u64" + }, + { + "name": "real_sol_reserves", + "type": "u64" + } + ] + } + }, + { + "name": "UserVolumeAccumulator", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "needs_claim", + "type": "bool" + }, + { + "name": "total_unclaimed_tokens", + "type": "u64" + }, + { + "name": "total_claimed_tokens", + "type": "u64" + }, + { + "name": "current_sol_volume", + "type": "u64" + }, + { + "name": "last_update_timestamp", + "type": "i64" + }, + { + "name": "has_total_claimed_tokens", + "type": "bool" + }, + { + "name": "cashback_earned", + "type": "u64" + }, + { + "name": "total_cashback_claimed", + "type": "u64" + }, + { + "name": "stable_cashback_earned", + "type": "u64" + }, + { + "name": "total_stable_cashback_claimed", + "type": "u64" + } + ] + } + } + ] +} diff --git a/crates/core/src/scenarios/protocols/pump/v1/mod.rs b/crates/core/src/scenarios/protocols/pump/v1/mod.rs new file mode 100644 index 000000000..8f3237e48 --- /dev/null +++ b/crates/core/src/scenarios/protocols/pump/v1/mod.rs @@ -0,0 +1 @@ +pub mod graduation_builder; diff --git a/crates/core/src/scenarios/protocols/pump/v1/overrides.yaml b/crates/core/src/scenarios/protocols/pump/v1/overrides.yaml new file mode 100644 index 000000000..e0172c812 --- /dev/null +++ b/crates/core/src/scenarios/protocols/pump/v1/overrides.yaml @@ -0,0 +1,164 @@ +protocol: Pump +version: v1 +account_type: BondingCurve +idl_file_path: idl.json + +tags: + - amm + - bonding-curve + - launchpad + - defi + +constants: + # Token mints loaded from verified tokens registry + token_mint: + label: Token + description: Select a pump.fun coin mint address from verified tokens + source: verified_tokens + address_suffix: pump + +templates: + - id: pump-bonding-curve-custom + name: Override Bonding Curve (Custom) + description: | + Override the bonding curve of any pump.fun coin by specifying its mint. + The bonding curve address is a PDA derived from ["bonding-curve", mint]. + idl_account_name: BondingCurve + properties: + - path: virtual_token_reserves + label: Virtual Token Reserves + description: Synthetic token reserves driving the price formula (raw units, 6 decimals) + - path: virtual_quote_reserves + label: Virtual Quote Reserves + description: Synthetic quote reserves driving the price formula (lamports for SOL-quoted coins) + - path: real_token_reserves + label: Real Token Reserves + description: Tokens actually held by the curve; the curve completes when this reaches 0 + - path: real_quote_reserves + label: Real Quote Reserves + description: Quote actually held by the curve (lamports for SOL-quoted coins) + - path: complete + label: Complete + description: True once the curve is bought out and ready to migrate to PumpSwap + - path: creator + label: Creator + description: Coin creator that accrues creator fees via the creator vault + - path: token_mint + type: constant_ref + label: Token Mint + constant: token_mint + llm_context: | + Set fetchBeforeUse: true so the fields you don't override keep their live values. + Use false only for a later override that builds on state an earlier one prepared in + the same scenario. + + ONLY pump.fun coins have a bonding curve account. Their mint addresses usually end + with "pump". Selecting any other mint derives an address that holds no account, so + the override has nothing to apply to. + + HOW THE CURVE PRICES (constant product over synthetic reserves, Uniswap V2 style): + - spot price in quote-per-token raw units = virtual_quote_reserves / virtual_token_reserves + - pump.fun coins have 6 decimals; the classic quote is SOL in lamports (9 decimals) + + INVARIANTS THE PROGRAM MAINTAINS (keep them consistent when overriding): + - a buy increases virtual_quote_reserves and real_quote_reserves by the same amount + and decreases virtual_token_reserves and real_token_reserves by the same amount; + a sell does the exact reverse + - therefore virtual_token_reserves - real_token_reserves never changes over the life + of a curve (279,900,000,000,000 with today's mainnet Global defaults), and + virtual_quote_reserves - real_quote_reserves stays at the initial virtual quote + (30,000,000,000 lamports today) + - complete flips to true when real_token_reserves reaches 0; a completed curve + rejects buy and sell and can only be migrated to a PumpSwap pool + + NEW CURVES START FROM Global's initial values: 1,073,000,000,000,000 virtual tokens, + 30,000,000,000 virtual quote lamports, 793,100,000,000,000 real tokens, 0 real quote, + and a 1,000,000,000,000,000 total supply. Override a curve to exactly these (with + complete: false) to reopen an already-migrated coin for trading on its curve — and + set its vault back to the full 1,000,000,000,000,000 total supply (real reserves + plus migration reserve; see below), because migration drained it. + + THE CURVE VAULT IS COUPLED STATE. Whenever an override changes real_token_reserves — + lowering it toward graduation or raising it to reopen a curve — also override the + curve's token vault (the curve PDA's associated token account under the mint's + token program) with spl-token-account-balance: buys pay tokens out of the vault, and + its amount must equal the new real_token_reserves plus the coin's migration reserve + (206,900,000,000,000 with today's Global defaults), never real_token_reserves alone — + migration then has nothing to seed the PumpSwap pool with and fails. The graduation + builder derives and sets it automatically. + + total_fee_bps = protocol fee + creator fee (0 when the curve has no creator). + Trades read both from the fee program's FeeConfig market-cap fee tiers — a + required account of every buy and sell. Global.fee_basis_points and + Global.creator_fee_basis_points are legacy fields from before the external fee + program. The creator share accrues to the curve's creator vault. + + EXAMPLE - "prove a completed curve rejects trading": one override, values = + token_mint: + real_token_reserves: 0 + complete: true + A buy against it now fails with BondingCurveComplete (0x1775). + address: + type: pda + program_id: 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P + seeds: + - type: string + value: bonding-curve + - type: property_ref + value: token_mint + + - id: pump-global + name: Override Global Configuration + description: | + Override the Pump program's single Global configuration account + (PDA derived from ["global"], resolving to 4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf). + idl_account_name: Global + properties: + - path: fee_basis_points + label: Fee Basis Points (legacy) + description: Legacy protocol fee in basis points, from before the external fee program; live trades read fees from the fee program's FeeConfig instead + - path: creator_fee_basis_points + label: Creator Fee Basis Points (legacy) + description: Legacy creator fee in basis points, likewise superseded by the external fee program + - path: initial_virtual_token_reserves + label: Initial Virtual Token Reserves + description: Virtual token reserves a newly created curve starts with; parameterizes new curves only, existing curves keep their creation values + - path: initial_virtual_sol_reserves + label: Initial Virtual SOL Reserves + description: Virtual SOL reserves seeding classic SOL-quoted curves at creation + - path: initial_virtual_quote_reserves + label: Initial Virtual Quote Reserves + description: Virtual quote reserves seeding newer non-SOL quote-mint curves at creation + - path: initial_real_token_reserves + label: Initial Real Token Reserves + description: Real token reserves a new curve starts with, the tokens available to buy before it completes + - path: token_total_supply + label: Token Total Supply + description: Total token supply minted for a new curve + - path: enable_migrate + label: Enable Migrate + description: Gates the migrate instruction that moves a completed curve's liquidity to PumpSwap + - path: pool_migration_fee + label: Pool Migration Fee + description: Fee taken when a completed curve migrates to its PumpSwap pool + - path: withdraw_authority + label: Withdraw Authority + description: Authority the migrate and withdraw paths check; override it to a key you control to drive a real migrate transaction on a fork + llm_context: | + Set fetchBeforeUse: true so Global's other fields (authorities, fee recipient lists) + keep their live values; false only to build on an earlier override's prepared state. + + The initial_* values only parameterize bonding curves created AFTER the override; + existing curves keep the values they were created with. initial_virtual_sol_reserves + seeds classic SOL-quoted curves, initial_virtual_quote_reserves the newer non-SOL + quote-mint curves. fee_basis_points and creator_fee_basis_points are legacy + fields from before the external fee program; trades read fees from the fee + program's FeeConfig — a required account of every buy and sell. enable_migrate + gates the migrate instruction that moves a completed curve's liquidity to + PumpSwap. + address: + type: pda + program_id: 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P + seeds: + - type: string + value: global diff --git a/crates/core/src/scenarios/registry.rs b/crates/core/src/scenarios/registry.rs index 9d69b0eee..c426d3777 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -32,6 +32,13 @@ pub const WHIRLPOOL_OVERRIDES_CONTENT: &str = include_str!("./protocols/whirlpoo pub const SPL_TOKEN_IDL_CONTENT: &str = include_str!("./protocols/spl-token/idl.json"); pub const SPL_TOKEN_OVERRIDES_CONTENT: &str = include_str!("./protocols/spl-token/overrides.yaml"); +pub const PUMP_V1_IDL_CONTENT: &str = include_str!("./protocols/pump/v1/idl.json"); +pub const PUMP_V1_OVERRIDES_CONTENT: &str = include_str!("./protocols/pump/v1/overrides.yaml"); + +pub const PUMP_AMM_V1_IDL_CONTENT: &str = include_str!("./protocols/pump-amm/v1/idl.json"); +pub const PUMP_AMM_V1_OVERRIDES_CONTENT: &str = + include_str!("./protocols/pump-amm/v1/overrides.yaml"); + /// Registry for managing override templates loaded from YAML files #[derive(Clone, Debug, Default)] pub struct TemplateRegistry { @@ -51,6 +58,7 @@ impl TemplateRegistry { default.load_drift_overrides(); default.load_whirlpool_overrides(); default.load_spl_token_overrides(); + default.load_pump_overrides(); default } @@ -111,6 +119,15 @@ impl TemplateRegistry { ); } + pub fn load_pump_overrides(&mut self) { + self.load_protocol_overrides(PUMP_V1_IDL_CONTENT, PUMP_V1_OVERRIDES_CONTENT, "pump"); + self.load_protocol_overrides( + PUMP_AMM_V1_IDL_CONTENT, + PUMP_AMM_V1_OVERRIDES_CONTENT, + "pump-amm", + ); + } + fn load_protocol_overrides( &mut self, idl_content: &str, @@ -325,15 +342,95 @@ mod tests { ); } + /// Both singleton addresses are documented in pump-public-docs: the Pump Global + /// account at 4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf and the PumpSwap + /// GlobalConfig at ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw. + #[test] + fn pump_singletons_derive_their_documented_addresses() { + let registry = TemplateRegistry::new(); + + let global = registry.get("pump-global").expect("template"); + assert_eq!( + global.address.resolve(None).expect("resolves"), + Pubkey::from_str("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf").expect("address"), + ); + + let config = registry.get("pump-amm-global-config").expect("template"); + assert_eq!( + config.address.resolve(None).expect("resolves"), + Pubkey::from_str("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw").expect("address"), + ); + } + + /// The expected addresses are not ours. pump-public-docs (PUMP_SWAP_README.md) + /// documents the canonical pool GseMAnNDvntR5uFePZ51yZBXzNSn7GdFPkfHwfr6d77J of the + /// migrated coin 7LSsEoJG…pump, with the Pump pool-authority PDA 9XDYTfQK… as its + /// creator, so deriving both pins the whole canonical chain: index 0 as u16 LE, the + /// nested pool-authority PDA, the base mint, and wrapped SOL. The bonding curve + /// address was checked against mainnet on 2026-08-06: owner 6EF8rrec…, discriminator + /// 23 183 248 55 96 216 172 96 (BondingCurve), complete = true. + #[test] + fn pump_templates_derive_the_documented_migrated_coin_accounts() { + let registry = TemplateRegistry::new(); + let mint = "7LSsEoJGhLeZzGvDofTdNg7M3JttxQqGWNLo6vWMpump"; + + let curve = registry.get("pump-bonding-curve-custom").expect("template"); + let values = HashMap::from([( + "token_mint".to_string(), + serde_json::Value::String(mint.to_string()), + )]); + assert_eq!( + curve.address.resolve(Some(&values)).expect("resolves"), + Pubkey::from_str("3MUkKMbuornHohtAtzrToSzqkj1gEEhQqYVz8sZnmQg1").expect("address"), + ); + + let pool = registry.get("pump-amm-canonical-pool").expect("template"); + let values = HashMap::from([( + "base_mint".to_string(), + serde_json::Value::String(mint.to_string()), + )]); + + let AccountAddress::Pda { seeds, .. } = &pool.address else { + panic!("the pool address is a PDA"); + }; + let pool_authority = seeds + .iter() + .find(|seed| matches!(seed, PdaSeed::DerivedPda { .. })) + .expect("the pool PDA derives the pool authority PDA") + .to_bytes(Some(&values)) + .expect("the pool authority resolves"); + assert_eq!( + Pubkey::try_from(pool_authority.as_slice()).expect("32 bytes"), + Pubkey::from_str("9XDYTfQKwW8sHPqnFdUreMmtmffmkHVPGTNV2e3LKxNW").expect("address"), + ); + + assert_eq!( + pool.address.resolve(Some(&values)).expect("resolves"), + Pubkey::from_str("GseMAnNDvntR5uFePZ51yZBXzNSn7GdFPkfHwfr6d77J").expect("address"), + ); + } + + #[test] + fn pump_pool_address_needs_every_seed_to_resolve() { + let registry = TemplateRegistry::new(); + let template = registry.get("pump-amm-canonical-pool").expect("template"); + + assert_eq!( + template.address.resolve(Some(&HashMap::new())), + None, + "a missing base mint must not derive a shorter address" + ); + } + #[test] fn test_registry_loads_all_protocols() { let registry = TemplateRegistry::new(); - // Should have Pyth (1 template) + Jupiter (1) + Raydium CLMM (1) + Raydium AMM v4 (4) + Drift(4) + Meteora (2) + Kamino(3) + Whirlpool(6) + SPL Token (2) = 24 total + // Should have Pyth (1 template) + Jupiter (1) + Raydium CLMM (1) + Raydium AMM v4 (4) + Drift(4) + Meteora (2) + Kamino(3) + Whirlpool(6) + SPL Token (2) + Pump (2) + PumpSwap (3) = 29 total assert_eq!( registry.count(), - 24, - "Registry should load 24 templates total" + 29, + "Registry should load 29 templates total" ); assert!(registry.contains("pyth-price-feed-v2")); @@ -368,6 +465,13 @@ mod tests { assert!(registry.contains("spl-token-account-balance")); assert!(registry.contains("spl-token-mint-supply")); + + assert!(registry.contains("pump-bonding-curve-custom")); + assert!(registry.contains("pump-global")); + + assert!(registry.contains("pump-amm-pool-state")); + assert!(registry.contains("pump-amm-canonical-pool")); + assert!(registry.contains("pump-amm-global-config")); } #[test] @@ -418,6 +522,16 @@ mod tests { 6, "Should have 6 Whirlpool templates" ); + + let pump_templates = registry.by_protocol("Pump"); + assert_eq!(pump_templates.len(), 2, "Should have 2 Pump templates"); + + let pump_swap_templates = registry.by_protocol("PumpSwap"); + assert_eq!( + pump_swap_templates.len(), + 3, + "Should have 3 PumpSwap templates" + ); } #[test] @@ -503,25 +617,26 @@ mod tests { token_mint_constant.options.len() ); - // Check that common tokens are present with correct addresses + // Check that common tokens are present, keyed by their mint address let sol_option = token_mint_constant .options .iter() - .find(|o| o.id == "sol") + .find(|o| o.value == "So11111111111111111111111111111111111111112") .expect("SOL token should be present"); assert_eq!( - sol_option.value, "So11111111111111111111111111111111111111112", - "SOL address should match" + sol_option.id, sol_option.value, + "option ids are mint addresses so colliding symbols keep every mint" ); let usdc_option = token_mint_constant .options .iter() - .find(|o| o.id == "usdc") + .find(|o| o.value == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") .expect("USDC token should be present"); assert_eq!( - usdc_option.value, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", - "USDC address should match" + usdc_option.metadata.get("symbol").and_then(|v| v.as_str()), + Some("USDC"), + "USDC symbol should be in metadata" ); // Check metadata is populated diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index e7b7a6d24..6a00a86ba 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -154,6 +154,24 @@ impl AccountUpdatePolicy { } } +/// Token accounts carry no Anchor discriminator, so the IDL forge path cannot decode them. +fn forge_token_account_data( + account: &Account, + mut token_account: TokenAccount, + account_values: &HashMap, +) -> SurfpoolResult> { + let amount = account_values + .get("amount") + .and_then(|amount| { + amount + .as_u64() + .or_else(|| amount.as_str().and_then(|amount| amount.parse().ok())) + }) + .ok_or_else(|| SurfpoolError::internal("amount must be an unsigned 64-bit integer"))?; + token_account.set_amount(amount); + token_account.pack_into_preserving_extensions(&account.data) +} + /// Helper function to apply an override to a decoded account value using dot notation pub fn apply_override_to_decoded_account( decoded_value: &mut Value, @@ -2700,37 +2718,77 @@ impl SurfnetSvm { account_pubkey ); - match client + let fetched = match client .get_account(&account_pubkey, CommitmentConfig::confirmed()) .await { Ok(GetAccountResult::FoundAccount(_pubkey, remote_account, _)) => { - debug!( - "Fetched account {} from remote: {} lamports, {} bytes", - account_pubkey, - remote_account.lamports(), - remote_account.data().len() - ); - - // Set the fresh account data in the SVM - if let Err(e) = self.inner.set_account(account_pubkey, remote_account) { - warn!( - "Failed to set account {} from remote: {}", - account_pubkey, e - ); - } + Some((remote_account, None)) } + Ok(GetAccountResult::FoundCoupledAccount( + (_pubkey, remote_account), + coupled, + _, + )) => Some(( + remote_account, + match coupled { + CoupledAccount::ProgramData(pubkey, account) + | CoupledAccount::Mint(pubkey, account) => { + account.map(|account| (pubkey, account)) + } + }, + )), Ok(GetAccountResult::None(_)) => { debug!("Account {} not found on remote", account_pubkey); - } - Ok(_) => { - debug!("Account {} fetched (other variant)", account_pubkey); + None } Err(e) => { warn!( "Failed to fetch account {} from remote: {}", account_pubkey, e ); + None + } + }; + + if let Some((remote_account, coupled)) = fetched { + debug!( + "Fetched account {} from remote: {} lamports, {} bytes", + account_pubkey, + remote_account.lamports(), + remote_account.data().len() + ); + + // The coupled account was not asked for: fill a fork gap, + // never clobber local state. + if let Some((coupled_pubkey, coupled_account)) = coupled { + match self.inner.get_account(&coupled_pubkey) { + Ok(None) => { + if let Err(e) = + self.inner.set_account(coupled_pubkey, coupled_account) + { + warn!( + "Failed to set coupled account {} from remote: {}", + coupled_pubkey, e + ); + } + } + Ok(Some(_)) => {} + Err(e) => { + warn!( + "Failed to read coupled account {}: {}", + coupled_pubkey, e + ); + } + } + } + + // Set the fresh account data in the SVM + if let Err(e) = self.inner.set_account(account_pubkey, remote_account) { + warn!( + "Failed to set account {} from remote: {}", + account_pubkey, e + ); } } } else { @@ -2777,6 +2835,23 @@ impl SurfnetSvm { continue; }; + // Mints fail the token unpack and keep flowing through the IDL path. + if is_supported_token_program(account.owner()) { + if let Ok(token_account) = TokenAccount::unpack(account.data()) { + let new_account_data = + forge_token_account_data(&account, token_account, &account_values)?; + let modified_account = Account { + lamports: account.lamports(), + data: new_account_data, + owner: *account.owner(), + executable: account.executable(), + rent_epoch: account.rent_epoch(), + }; + self.inner.set_account(account_pubkey, modified_account)?; + continue; + } + } + // Get the account owner (program ID) let owner_program_id = account.owner(); @@ -4238,6 +4313,174 @@ mod tests { assert!(!startup.has_changed().unwrap()); } + /// A Token-2022 vault with a fake extension tail. The forge helper never + /// unpacks the tail, so its bytes only need to be distinguishable. + fn token_2022_vault_with_tail(mint: Pubkey) -> (crate::types::TokenAccount, Account) { + let mut token_account = crate::types::TokenAccount::new( + &spl_token_2022_interface::id(), + Pubkey::new_unique(), + mint, + None, + ); + token_account.set_amount(10); + let mut data = token_account.pack_into_vec(); + data.extend_from_slice(&[2, 1, 2, 3, 4]); + let account = Account { + lamports: 2_039_280, + data, + owner: spl_token_2022_interface::id(), + executable: false, + rent_epoch: 0, + }; + (token_account, account) + } + + #[test] + fn token_account_override_patches_only_the_amount_bytes() { + let (token_account, account) = token_2022_vault_with_tail(Pubkey::new_unique()); + // Studio clients send u64 values as strings, so the parse path is the contract. + let account_values = HashMap::from([("amount".to_string(), serde_json::json!("42"))]); + + let patched = forge_token_account_data(&account, token_account, &account_values).unwrap(); + + assert_eq!(patched.len(), account.data.len()); + assert_eq!(&patched[64..72], &42u64.to_le_bytes()); + assert_eq!(&patched[..64], &account.data[..64]); + assert_eq!(&patched[72..], &account.data[72..]); + } + + /// Minimal JSON-RPC stand-in that answers every request with one canned `result` body, so + /// the remote-fetch branches can be exercised without a network. + async fn canned_rpc(result_json: &'static str) -> String { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind canned rpc"); + let addr = listener.local_addr().expect("local addr"); + + tokio::spawn(async move { + while let Ok((mut stream, _)) = listener.accept().await { + tokio::spawn(async move { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + let mut buf = vec![0u8; 16 * 1024]; + let _ = stream.read(&mut buf).await; + let body = format!(r#"{{"jsonrpc":"2.0","result":{result_json},"id":1}}"#); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + let _ = stream.write_all(response.as_bytes()).await; + let _ = stream.flush().await; + }); + } + }); + + format!("http://{addr}") + } + + /// A 165-byte SPL token account (state = Initialized), which sends `get_account` down the + /// coupled-mint path. The canned server answers the mint lookup with the same body, and the + /// account's zeroed mint field makes the coupled mint land on the default pubkey. + const CANNED_TOKEN_ACCOUNT: &str = concat!( + r#"{"context":{"apiVersion":"2.1.0","slot":1},"value":{"data":[""#, + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + r#"","base64"],"executable":false,"lamports":2039280,"#, + r#""owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":0,"space":165}}"# + ); + + fn fetch_before_use_scenario(target: Pubkey) -> surfpool_types::Scenario { + let mut scenario = surfpool_types::Scenario::new( + "coupled fetch".to_string(), + "fetch_before_use must fork the target and its coupled account".to_string(), + ); + let mut instance = surfpool_types::OverrideInstance::new( + "spl-token-account-balance".to_string(), + 0, + surfpool_types::AccountAddress::Pubkey(target.to_string()), + ); + instance.fetch_before_use = true; + scenario.add_override(instance); + scenario + } + + /// Token and executable accounts return `FoundCoupledAccount`. That arm used to fall through + /// a catch-all that logged and dropped the account, so the fetch reported success while the + /// target was never forked. + #[tokio::test(flavor = "multi_thread")] + async fn test_fetch_before_use_materializes_a_coupled_account() { + let url = canned_rpc(CANNED_TOKEN_ACCOUNT).await; + let remote = (SurfnetRemoteClient::new(url), CommitmentConfig::confirmed()); + let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); + let locker = crate::surfnet::locker::SurfnetSvmLocker::new(svm); + let target = Pubkey::new_unique(); + + locker + .register_scenario(fetch_before_use_scenario(target), Some(100)) + .unwrap(); + locker + .materialize_overrides_for_slot(&Some(remote), 100) + .await + .unwrap(); + + let fetched = locker + .with_svm_reader(|svm_reader| svm_reader.get_account(&target)) + .unwrap(); + assert!( + fetched.is_some(), + "the fetched token account must be forked" + ); + let coupled_mint = locker + .with_svm_reader(|svm_reader| svm_reader.get_account(&Pubkey::default())) + .unwrap(); + assert!( + coupled_mint.is_some(), + "the coupled mint must fill the gap in the fork" + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_fetch_before_use_keeps_a_locally_modified_coupled_account() { + let url = canned_rpc(CANNED_TOKEN_ACCOUNT).await; + let remote = (SurfnetRemoteClient::new(url), CommitmentConfig::confirmed()); + let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); + let locker = crate::surfnet::locker::SurfnetSvmLocker::new(svm); + let target = Pubkey::new_unique(); + + let marker = vec![7u8; 82]; + locker.with_svm_writer(|svm_writer| { + svm_writer + .set_account( + &Pubkey::default(), + Account { + lamports: 1_000_000, + data: marker.clone(), + owner: spl_token_interface::id(), + executable: false, + rent_epoch: 0, + }, + ) + .unwrap(); + }); + + locker + .register_scenario(fetch_before_use_scenario(target), Some(100)) + .unwrap(); + locker + .materialize_overrides_for_slot(&Some(remote), 100) + .await + .unwrap(); + + let mint = locker + .with_svm_reader(|svm_reader| svm_reader.get_account(&Pubkey::default())) + .unwrap() + .unwrap(); + assert_eq!( + mint.data, marker, + "only the explicitly refreshed target may be overwritten; the coupled account was \ + not requested and must keep its local state" + ); + } + fn build_transfer_transaction( payer: &Keypair, recipient: &Pubkey, diff --git a/crates/core/src/tests/integration.rs b/crates/core/src/tests/integration.rs index 00b04447b..fa182dcc8 100644 --- a/crates/core/src/tests/integration.rs +++ b/crates/core/src/tests/integration.rs @@ -86,6 +86,7 @@ use surfpool_types::{ use test_case::test_case; use tokio::{sync::RwLock, task}; use uuid::Uuid; + pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; use crate::{ @@ -127,7 +128,7 @@ const STOP_DEADLINE: Duration = Duration::from_secs(15); /// never starts and a runloop that never stops present the same way: a test /// that does not finish. #[derive(Debug)] -enum RunloopError { +pub(crate) enum RunloopError { /// The OS refused a thread. This is the shape thread exhaustion arrives /// in, so it is the first thing to fail if the guards below stop working. Spawn(std::io::Error), @@ -170,7 +171,7 @@ impl std::fmt::Display for RunloopError { impl std::error::Error for RunloopError {} /// A running surfnet and the thread it runs on. Dropping it stops the runloop. -struct RunloopGuard { +pub(crate) struct RunloopGuard { commands: Sender, thread: Option>, } @@ -238,7 +239,7 @@ impl Drop for RunloopGuard { /// Bind the guard for as long as the test needs the surfnet, conventionally /// `let _runloop = spawn_runloop(...)`. A plain `let _ =` drops it there and /// then, which stops the runloop before the test has used it. -fn spawn_runloop( +pub(crate) fn spawn_runloop( svm_locker: SurfnetSvmLocker, config: SurfpoolConfig, commands: ( @@ -273,7 +274,7 @@ fn spawn_runloop( /// Waits for the surfnet to say it is ready and that it reached its /// datasource. -fn wait_for_ready_and_connected( +pub(crate) fn wait_for_ready_and_connected( simnet_events_rx: &crossbeam_channel::Receiver, ) -> Result<(), RunloopError> { wait_for_startup(simnet_events_rx, Connection::Required) diff --git a/crates/core/src/tests/mod.rs b/crates/core/src/tests/mod.rs index 01bc99f08..54093ec1e 100644 --- a/crates/core/src/tests/mod.rs +++ b/crates/core/src/tests/mod.rs @@ -1,4 +1,6 @@ pub mod helpers; pub mod integration; pub mod plugin; +#[cfg(feature = "integration-tests")] +pub mod pump; pub mod simnet_events; diff --git a/crates/core/src/tests/pump/mod.rs b/crates/core/src/tests/pump/mod.rs new file mode 100644 index 000000000..960ce8e2f --- /dev/null +++ b/crates/core/src/tests/pump/mod.rs @@ -0,0 +1,1102 @@ +//! Pump / PumpSwap integration tests. +//! +//! These fetch the real accounts from mainnet rather than embedding captured copies, so they +//! need a network connection and are compiled only behind a feature: +//! +//! ```text +//! cargo test -p surfpool-core --features integration-tests pump +//! ``` +//! +//! Set `SURFPOOL_TEST_RPC_URL` to use a private endpoint if the public one rate-limits. +//! +//! What these cover that unit tests cannot: real accounts carry live values, populated +//! creator fields and the `extend_account` tail past the Borsh layout, so a pump program +//! upgrade that changes the on-chain layout shows up as a byte diff here and nowhere else. +//! +//! The lifecycle test starts a surfnet that forks mainnet and discovers a fresh, still-trading +//! Token-2022 coin from the pump program's recent transactions — no fixed coin stays +//! incomplete, so the candidate is found at test time and the fork freezes its state on first +//! read. The pump and pAMM programs run live, so a behavior-changing upgrade fails here first. + +use std::collections::HashMap; + +use crossbeam_channel::unbounded; +use solana_account::Account; +use solana_account_decoder::UiAccountEncoding; +use solana_client::{ + nonblocking::rpc_client::RpcClient, + rpc_config::{RpcSimulateTransactionAccountsConfig, RpcSimulateTransactionConfig}, + rpc_request::RpcRequest, +}; +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 solana_signer::Signer; +use solana_transaction::Transaction; +use surfpool_types::{OverrideInstance, RpcConfig, Scenario, SimnetConfig, SurfpoolConfig}; + +use crate::{ + scenarios::{ + TemplateRegistry, + protocols::pump::v1::graduation_builder::{ + PumpGraduationPreparation, build_pump_graduation_scenario, pump_graduation_addresses, + }, + }, + storage::tests::TestType, + surfnet::{ + GetAccountResult, locker::SurfnetSvmLocker, remote::SurfnetRemoteClient, svm::SurfnetSvm, + }, + tests::{ + helpers::get_free_port, + integration::{RunloopGuard, spawn_runloop, wait_for_ready_and_connected}, + }, +}; + +const RPC_URL_ENV: &str = "SURFPOOL_TEST_RPC_URL"; +const DEFAULT_RPC_URL: &str = "https://api.mainnet-beta.solana.com"; + +const PUMP: Pubkey = Pubkey::from_str_const("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); +const PAMM: Pubkey = Pubkey::from_str_const("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"); +const FEE_PROGRAM: Pubkey = Pubkey::from_str_const("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"); +const TOKEN_2022: Pubkey = Pubkey::from_str_const("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); +const TOKENKEG: Pubkey = Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); +const WSOL: Pubkey = Pubkey::from_str_const("So11111111111111111111111111111111111111112"); +const ATA_PROGRAM: Pubkey = Pubkey::from_str_const("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); +const SYSTEM_PROGRAM: Pubkey = Pubkey::from_str_const("11111111111111111111111111111111"); +const RENT_SYSVAR: Pubkey = Pubkey::from_str_const("SysvarRent111111111111111111111111111111111"); +const USDC_MINT: Pubkey = Pubkey::from_str_const("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + +const MINT: Pubkey = Pubkey::from_str_const("HRTzNRJNnY78xe8e4a9DuMotw6qA97GwSQLzpVw9pump"); +const CURVE: Pubkey = Pubkey::from_str_const("GBpTHrtF8dGwxC7thRD7T6VfGtbVYEabKkQ7k6g3u7QF"); +const BASE_VAULT: Pubkey = Pubkey::from_str_const("9sXf9hAtryY1mncMxKGZnLMJzQbnTsUoSu8GJTX3FpFh"); +const PUMP_GLOBAL: Pubkey = Pubkey::from_str_const("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); +const AMM_GLOBAL_CONFIG: Pubkey = + Pubkey::from_str_const("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"); + +/// The bonding curve of the migrated coin 7LSsEoJG…pump, documented in pump-public-docs and +/// checked against mainnet on 2026-08-06 (complete = true, so it never changes again). +const LLS_CURVE: Pubkey = Pubkey::from_str_const("3MUkKMbuornHohtAtzrToSzqkj1gEEhQqYVz8sZnmQg1"); +/// The canonical PumpSwap pool of the same migrated coin. +const AMM_POOL: Pubkey = Pubkey::from_str_const("GseMAnNDvntR5uFePZ51yZBXzNSn7GdFPkfHwfr6d77J"); + +const BUY_V2_DISCRIMINATOR: [u8; 8] = [184, 23, 238, 97, 103, 197, 211, 61]; +const MIGRATE_V2_DISCRIMINATOR: [u8; 8] = [187, 203, 18, 31, 206, 237, 254, 41]; +const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173]; + +const MAX_SOL_COST: u64 = 1_000_000_000; + +const CURVE_VIRTUAL_TOKEN_RESERVES_OFFSET: usize = 8; +const CURVE_VIRTUAL_QUOTE_RESERVES_OFFSET: usize = 16; +const CURVE_REAL_TOKEN_RESERVES_OFFSET: usize = 24; +const CURVE_REAL_QUOTE_RESERVES_OFFSET: usize = 32; +const CURVE_COMPLETE_OFFSET: usize = 48; +const CURVE_CREATOR_OFFSET: usize = 49; +const CURVE_QUOTE_MINT_OFFSET: usize = 83; +const GLOBAL_FEE_RECIPIENT_OFFSET: usize = 41; +const GLOBAL_WITHDRAW_AUTHORITY_OFFSET: usize = 113; +const GLOBAL_BUYBACK_RECIPIENTS_OFFSET: usize = 741; +const TOKEN_AMOUNT_OFFSET: usize = 64; +const AMM_PROTOCOL_FEE_RECIPIENTS_OFFSET: usize = 57; +const POOL_LP_SUPPLY_OFFSET: usize = 203; +const POOL_VIRTUAL_QUOTE_RESERVES_OFFSET: usize = 245; +const POOL_COIN_CREATOR_OFFSET: usize = 211; + +/// Fetches the accounts in one request, so every account returned is from the same slot. +async fn fetch(addresses: &[Pubkey]) -> Vec { + let client = SurfnetRemoteClient::new( + std::env::var(RPC_URL_ENV).unwrap_or_else(|_| DEFAULT_RPC_URL.to_string()), + ); + + client + .get_multiple_accounts(addresses, CommitmentConfig::confirmed()) + .await + .unwrap_or_else(|e| panic!("failed to fetch {addresses:?} from mainnet: {e}")) + .into_iter() + .zip(addresses) + .map(|(result, address)| match result { + GetAccountResult::FoundAccount(_, account, _) + | GetAccountResult::FoundCoupledAccount((_, account), _, _) => account, + GetAccountResult::None(_) => { + panic!("{address} no longer exists on mainnet; the test needs a new address") + } + }) + .collect() +} + +/// Byte indices at which two buffers differ. +fn diff_indices(a: &[u8], b: &[u8]) -> Vec { + a.iter() + .zip(b.iter()) + .enumerate() + .filter(|(_, (x, y))| x != y) + .map(|(i, _)| i) + .collect() +} + +/// A failure here means a bundled IDL disagrees with the live on-chain layout. +#[tokio::test] +async fn real_mainnet_accounts_round_trip_unchanged() { + let cases: &[(&str, &str, Pubkey)] = &[ + ("pump-bonding-curve-custom", "BondingCurve", LLS_CURVE), + ("pump-bonding-curve-custom", "BondingCurve", CURVE), + ("pump-global", "Global", PUMP_GLOBAL), + ("pump-amm-canonical-pool", "Pool", AMM_POOL), + ("pump-amm-global-config", "GlobalConfig", AMM_GLOBAL_CONFIG), + ]; + + let addresses: Vec = cases.iter().map(|(_, _, a)| *a).collect(); + let accounts = fetch(&addresses).await; + + let (surfnet_svm, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default(); + let registry = TemplateRegistry::new(); + let pubkey = Pubkey::new_unique(); + + for ((template_id, account_name, address), account) in cases.iter().zip(&accounts) { + let template = registry + .get(template_id) + .unwrap_or_else(|| panic!("template {template_id} should exist")); + + let account_def = template + .idl + .accounts + .iter() + .find(|a| a.name == *account_name) + .unwrap_or_else(|| panic!("{account_name} not in the IDL")); + assert_eq!( + &account.data[..8], + account_def.discriminator.as_slice(), + "{address}: discriminator does not match the IDL - wrong account type?" + ); + + let forged = surfnet_svm + .get_forged_account_data(&pubkey, &account.data, &template.idl, &HashMap::new()) + .unwrap_or_else(|e| { + panic!( + "live mainnet {account_name} {address} failed to decode/re-encode with the \ + bundled IDL: {e}" + ) + }); + + assert_eq!( + forged.len(), + account.data.len(), + "{account_name} {address} changed size on round-trip" + ); + let diffs = diff_indices(&forged, &account.data); + assert!( + diffs.is_empty(), + "live mainnet {account_name} {address} was altered by a no-op round-trip at {} \ + byte(s), first at {:?}", + diffs.len(), + diffs.first() + ); + } +} + +/// Catches collateral damage from the Borsh re-encode against real bytes: only the overridden +/// fields may change, and the `extend_account` tail past the Borsh layout must survive. +#[tokio::test] +async fn override_on_real_account_touches_only_target_bytes() { + let accounts = fetch(&[LLS_CURVE, AMM_POOL]).await; + let (curve_data, pool_data) = (&accounts[0].data, &accounts[1].data); + + let (surfnet_svm, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default(); + let registry = TemplateRegistry::new(); + let pubkey = Pubkey::new_unique(); + + // A semantically valid just-completed curve: complete=true requires + // real_token_reserves = 0, and the curve-lifetime invariants hold + // (virtual - real stays at 279.9T tokens / 30 SOL of quote). + let curve = registry.get("pump-bonding-curve-custom").expect("template"); + let overrides = HashMap::from([ + ( + "virtual_token_reserves".to_string(), + serde_json::json!(279_900_000_000_000u64), + ), + ( + "virtual_quote_reserves".to_string(), + serde_json::json!(115_000_000_000u64), + ), + ("real_token_reserves".to_string(), serde_json::json!(0u64)), + ( + "real_quote_reserves".to_string(), + serde_json::json!(85_000_000_000u64), + ), + ("complete".to_string(), serde_json::json!(true)), + ]); + let forged = surfnet_svm + .get_forged_account_data(&pubkey, curve_data, &curve.idl, &overrides) + .expect("curve override on the live bonding curve"); + assert_eq!( + forged.len(), + curve_data.len(), + "the curve's extend_account tail must survive" + ); + let reserves = CURVE_VIRTUAL_TOKEN_RESERVES_OFFSET..CURVE_REAL_QUOTE_RESERVES_OFFSET + 8; + let diffs = diff_indices(&forged, curve_data); + assert!( + diffs + .iter() + .all(|i| reserves.contains(i) || *i == CURVE_COMPLETE_OFFSET), + "only the overridden curve fields may change, got {diffs:?}" + ); + assert_eq!( + read_u64(&forged, CURVE_VIRTUAL_TOKEN_RESERVES_OFFSET), + 279_900_000_000_000 + ); + assert_eq!( + read_u64(&forged, CURVE_VIRTUAL_QUOTE_RESERVES_OFFSET), + 115_000_000_000 + ); + assert_eq!(read_u64(&forged, CURVE_REAL_TOKEN_RESERVES_OFFSET), 0); + assert_eq!( + read_u64(&forged, CURVE_REAL_QUOTE_RESERVES_OFFSET), + 85_000_000_000 + ); + assert_eq!(forged[CURVE_COMPLETE_OFFSET], 1); + + let pool = registry.get("pump-amm-canonical-pool").expect("template"); + let overrides = HashMap::from([ + ("lp_supply".to_string(), serde_json::json!(9_876_543_210u64)), + ( + "virtual_quote_reserves".to_string(), + serde_json::json!(5_000_000_000i64), + ), + ]); + let forged = surfnet_svm + .get_forged_account_data(&pubkey, pool_data, &pool.idl, &overrides) + .expect("pool override on the live canonical pool"); + assert_eq!( + forged.len(), + pool_data.len(), + "the pool's extend_account tail must survive" + ); + let lp_supply = POOL_LP_SUPPLY_OFFSET..POOL_LP_SUPPLY_OFFSET + 8; + let virtual_quote = POOL_VIRTUAL_QUOTE_RESERVES_OFFSET..POOL_VIRTUAL_QUOTE_RESERVES_OFFSET + 16; + let diffs = diff_indices(&forged, pool_data); + assert!( + diffs + .iter() + .all(|i| lp_supply.contains(i) || virtual_quote.contains(i)), + "only lp_supply and the i128 virtual_quote_reserves may change, got {diffs:?}" + ); + assert_eq!(read_u64(&forged, POOL_LP_SUPPLY_OFFSET), 9_876_543_210); + assert_eq!( + i128::from_le_bytes( + forged[POOL_VIRTUAL_QUOTE_RESERVES_OFFSET..POOL_VIRTUAL_QUOTE_RESERVES_OFFSET + 16] + .try_into() + .unwrap() + ), + 5_000_000_000 + ); +} + +/// The production builder's validation against live pump state. Single-byte tweaks pin each +/// rejection branch regardless of where the live coin currently is in its lifecycle. +#[tokio::test] +async fn builder_rejects_bad_live_graduation_state() { + let accounts = fetch(&[MINT, CURVE, BASE_VAULT, PUMP_GLOBAL]).await; + let (mint, curve, vault, global) = (&accounts[0], &accounts[1], &accounts[2], &accounts[3]); + + let mut complete_curve = curve.clone(); + complete_curve.data[CURVE_COMPLETE_OFFSET] = 1; + let error = build_pump_graduation_scenario(MINT, mint, &complete_curve, vault, None, global) + .unwrap_err(); + assert!( + error.to_string().contains("already complete"), + "unexpected error: {error}" + ); + + let mut incomplete_curve = curve.clone(); + incomplete_curve.data[CURVE_COMPLETE_OFFSET] = 0; + let error = build_pump_graduation_scenario( + MINT, + mint, + &incomplete_curve, + vault, + Some(&Account::default()), + global, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("canonical PumpSwap pool already exists"), + "unexpected error: {error}" + ); + + let mut usdc_curve = incomplete_curve.clone(); + usdc_curve.data[CURVE_QUOTE_MINT_OFFSET..CURVE_QUOTE_MINT_OFFSET + 32] + .copy_from_slice(USDC_MINT.as_ref()); + let error = + build_pump_graduation_scenario(MINT, mint, &usdc_curve, vault, None, global).unwrap_err(); + assert!( + error.to_string().contains("SOL-quoted bonding curves only"), + "unexpected error: {error}" + ); +} + +/// Every graduation account, derived for the discovered coin: template PDAs through the +/// builder, instruction-only accounts through the IDL-documented seeds, and the fee wiring +/// read from the live `Global` so recipient rotation cannot break the test. +struct GraduationFixture { + mint: Pubkey, + curve: Pubkey, + base_vault: Pubkey, + quote_vault: Pubkey, + fee_recipient: Pubkey, + fee_recipient_quote: Pubkey, + buyback_recipient: Pubkey, + buyback_recipient_quote: Pubkey, + creator_vault: Pubkey, + creator_vault_quote: Pubkey, + sharing_config: Pubkey, + global_volume_accumulator: Pubkey, + fee_config: Pubkey, + withdraw_authority: Pubkey, + user: Pubkey, + user_base: Pubkey, + user_quote: Pubkey, + user_volume_accumulator: Pubkey, + user_volume_accumulator_quote: Pubkey, + pool_authority: Pubkey, + pool: Pubkey, + lp_mint: Pubkey, + pool_authority_base: Pubkey, + pool_authority_quote: Pubkey, + pool_authority_lp: Pubkey, + pool_base: Pubkey, + pool_quote: Pubkey, + pump_event_authority: Pubkey, + pamm_event_authority: Pubkey, + boost_vault_authority: Pubkey, + boost_vault: Pubkey, +} + +impl GraduationFixture { + fn new(user: Pubkey, mint: Pubkey, curve_data: &[u8], global_data: &[u8]) -> Self { + let addresses = + pump_graduation_addresses(&mint).expect("graduation addresses should resolve"); + let creator = + Pubkey::try_from(&curve_data[CURVE_CREATOR_OFFSET..CURVE_CREATOR_OFFSET + 32]) + .expect("curve creator"); + let creator_vault = + Pubkey::find_program_address(&[b"creator-vault", creator.as_ref()], &PUMP).0; + let fee_recipient = Pubkey::try_from( + &global_data[GLOBAL_FEE_RECIPIENT_OFFSET..GLOBAL_FEE_RECIPIENT_OFFSET + 32], + ) + .expect("global fee recipient"); + let buyback_recipient = Pubkey::try_from( + &global_data[GLOBAL_BUYBACK_RECIPIENTS_OFFSET..GLOBAL_BUYBACK_RECIPIENTS_OFFSET + 32], + ) + .expect("global buyback recipient"); + let withdraw_authority = Pubkey::try_from( + &global_data[GLOBAL_WITHDRAW_AUTHORITY_OFFSET..GLOBAL_WITHDRAW_AUTHORITY_OFFSET + 32], + ) + .expect("global withdraw authority"); + let user_volume_accumulator = + Pubkey::find_program_address(&[b"user_volume_accumulator", user.as_ref()], &PUMP).0; + let pool_authority = + Pubkey::find_program_address(&[b"pool-authority", mint.as_ref()], &PUMP).0; + let pool = Pubkey::find_program_address( + &[ + b"pool", + &0u16.to_le_bytes(), + pool_authority.as_ref(), + mint.as_ref(), + WSOL.as_ref(), + ], + &PAMM, + ) + .0; + let lp_mint = Pubkey::find_program_address(&[b"pool_lp_mint", pool.as_ref()], &PAMM).0; + let boost_vault_authority = + Pubkey::find_program_address(&[b"boost_vault", pool.as_ref()], &PAMM).0; + + Self { + mint, + curve: addresses.bonding_curve, + base_vault: addresses.curve_vault, + quote_vault: associated_token_address(&addresses.bonding_curve, &WSOL, &TOKENKEG), + fee_recipient, + fee_recipient_quote: associated_token_address(&fee_recipient, &WSOL, &TOKENKEG), + buyback_recipient, + buyback_recipient_quote: associated_token_address(&buyback_recipient, &WSOL, &TOKENKEG), + creator_vault, + creator_vault_quote: associated_token_address(&creator_vault, &WSOL, &TOKENKEG), + sharing_config: Pubkey::find_program_address( + &[b"sharing-config", mint.as_ref()], + &FEE_PROGRAM, + ) + .0, + global_volume_accumulator: Pubkey::find_program_address( + &[b"global_volume_accumulator"], + &PUMP, + ) + .0, + fee_config: Pubkey::find_program_address(&[b"fee_config", PUMP.as_ref()], &FEE_PROGRAM) + .0, + withdraw_authority, + user, + user_base: associated_token_address(&user, &mint, &TOKEN_2022), + user_quote: associated_token_address(&user, &WSOL, &TOKENKEG), + user_volume_accumulator, + user_volume_accumulator_quote: associated_token_address( + &user_volume_accumulator, + &WSOL, + &TOKENKEG, + ), + pool_authority, + pool, + lp_mint, + pool_authority_base: associated_token_address(&pool_authority, &mint, &TOKEN_2022), + pool_authority_quote: associated_token_address(&pool_authority, &WSOL, &TOKENKEG), + pool_authority_lp: associated_token_address(&pool_authority, &lp_mint, &TOKEN_2022), + pool_base: associated_token_address(&pool, &mint, &TOKEN_2022), + pool_quote: associated_token_address(&pool, &WSOL, &TOKENKEG), + pump_event_authority: Pubkey::find_program_address(&[b"__event_authority"], &PUMP).0, + pamm_event_authority: Pubkey::find_program_address(&[b"__event_authority"], &PAMM).0, + boost_vault_authority, + boost_vault: associated_token_address(&boost_vault_authority, &WSOL, &TOKENKEG), + } + } +} + +fn associated_token_address(owner: &Pubkey, mint: &Pubkey, token_program: &Pubkey) -> Pubkey { + Pubkey::find_program_address( + &[owner.as_ref(), token_program.as_ref(), mint.as_ref()], + &ATA_PROGRAM, + ) + .0 +} + +fn account_meta(pubkey: Pubkey, signer: bool, writable: bool) -> AccountMeta { + if writable { + AccountMeta::new(pubkey, signer) + } else { + AccountMeta::new_readonly(pubkey, signer) + } +} + +fn start_live_surfnet() -> (RpcClient, SurfnetSvmLocker, RunloopGuard) { + let bind_host = "127.0.0.1"; + let bind_port = get_free_port().unwrap(); + let ws_port = get_free_port().unwrap(); + let config = SurfpoolConfig { + simnets: vec![SimnetConfig { + remote_rpc_url: Some( + std::env::var(RPC_URL_ENV).unwrap_or_else(|_| DEFAULT_RPC_URL.to_string()), + ), + ..SimnetConfig::default() + }], + rpc: RpcConfig { + bind_host: bind_host.to_string(), + bind_port, + ws_port, + ..RpcConfig::default() + }, + ..SurfpoolConfig::default() + }; + let (surfnet_svm, simnet_events_rx, geyser_events_rx) = TestType::no_db().initialize_svm(); + let (simnet_commands_tx, simnet_commands_rx) = unbounded(); + let locker = SurfnetSvmLocker::new(surfnet_svm); + let runloop = spawn_runloop( + locker.clone(), + config, + (simnet_commands_tx, simnet_commands_rx), + geyser_events_rx, + ) + .expect("the runloop should start"); + wait_for_ready_and_connected(&simnet_events_rx) + .expect("surfnet should be ready and connected to the datasource"); + let rpc = RpcClient::new_with_commitment( + format!("http://{bind_host}:{bind_port}"), + CommitmentConfig::confirmed(), + ); + + (rpc, locker, runloop) +} + +/// Finds a fresh, still-incomplete Token-2022 pump coin in the pump program's most recent +/// mainnet transactions, reading its account graph through the surfnet (which also caches it +/// for the Play that follows). The builder's own validation is the eligibility filter. +async fn find_live_graduation_candidate( + surfnet: &RpcClient, +) -> (Pubkey, PumpGraduationPreparation) { + let mainnet = + RpcClient::new(std::env::var(RPC_URL_ENV).unwrap_or_else(|_| DEFAULT_RPC_URL.to_string())); + let signatures: serde_json::Value = mainnet + .send( + RpcRequest::GetSignaturesForAddress, + serde_json::json!([PUMP.to_string(), { "limit": 20 }]), + ) + .await + .expect("recent pump transactions should be listable"); + + let mut candidates: Vec = Vec::new(); + for entry in signatures.as_array().into_iter().flatten() { + let Some(signature) = entry["signature"].as_str() else { + continue; + }; + let Ok(transaction) = mainnet + .send::( + RpcRequest::GetTransaction, + serde_json::json!([ + signature, + { "encoding": "json", "maxSupportedTransactionVersion": 0 } + ]), + ) + .await + else { + continue; + }; + for key in transaction["transaction"]["message"]["accountKeys"] + .as_array() + .into_iter() + .flatten() + { + let Some(key) = key.as_str() else { continue }; + if key.ends_with("pump") && key != PUMP.to_string() { + if let Ok(mint) = key.parse::() { + if !candidates.contains(&mint) { + candidates.push(mint); + } + } + } + } + if candidates.len() >= 8 { + break; + } + } + + for mint in &candidates { + let Ok(addresses) = pump_graduation_addresses(mint) else { + continue; + }; + let pubkeys = [ + *mint, + addresses.bonding_curve, + addresses.curve_vault, + addresses.canonical_pool, + addresses.global, + ]; + let Ok(accounts) = surfnet.get_multiple_accounts(&pubkeys).await else { + continue; + }; + // A coin with a sharing config distributes creator fees to a shareholder + // list at migration; keep the test on the plain single-creator path. + let sharing_config = + Pubkey::find_program_address(&[b"sharing-config", mint.as_ref()], &FEE_PROGRAM).0; + if matches!(surfnet.get_account(&sharing_config).await, Ok(_)) { + continue; + } + let [ + Some(mint_account), + Some(curve), + Some(vault), + pool, + Some(global), + ] = &accounts[..5] + else { + continue; + }; + if let Ok(preparation) = + build_pump_graduation_scenario(*mint, mint_account, curve, vault, pool.as_ref(), global) + { + return (*mint, preparation); + } + } + + panic!( + "no eligible live pump coin among {} candidates from the last 20 pump transactions; \ + rerun the test", + candidates.len() + ); +} + +async fn cheatcode(rpc: &RpcClient, method: &'static str, params: serde_json::Value) { + let _: serde_json::Value = rpc + .send(RpcRequest::Custom { method }, params) + .await + .unwrap_or_else(|error| panic!("{method} cheatcode failed: {error:?}")); +} + +fn read_u64(data: &[u8], offset: usize) -> u64 { + u64::from_le_bytes( + data.get(offset..offset + 8) + .unwrap_or_else(|| panic!("missing u64 at account-data offset {offset}")) + .try_into() + .unwrap(), + ) +} + +async fn token_amount(rpc: &RpcClient, address: &Pubkey) -> u64 { + let account = rpc + .get_account(address) + .await + .unwrap_or_else(|error| panic!("token account {address} should exist: {error:?}")); + read_u64(&account.data, TOKEN_AMOUNT_OFFSET) +} + +async fn send_transaction(rpc: &RpcClient, payer: &Keypair, instructions: Vec) { + let transaction = signed_transaction(rpc, payer, instructions).await; + rpc.send_and_confirm_transaction(&transaction) + .await + .unwrap_or_else(|error| panic!("transaction failed: {error:?}")); +} + +async fn signed_transaction( + rpc: &RpcClient, + payer: &Keypair, + instructions: Vec, +) -> Transaction { + let blockhash = rpc + .get_latest_blockhash() + .await + .expect("recent blockhash should be available"); + Transaction::new_signed_with_payer(&instructions, Some(&payer.pubkey()), &[payer], blockhash) +} + +async fn simulate_token_amount_after_transaction( + rpc: &RpcClient, + payer: &Keypair, + instruction: Instruction, + token_account: Pubkey, +) -> u64 { + let transaction = signed_transaction(rpc, payer, vec![instruction]).await; + let simulation = rpc + .simulate_transaction_with_config( + &transaction, + RpcSimulateTransactionConfig { + sig_verify: true, + commitment: Some(CommitmentConfig::confirmed()), + accounts: Some(RpcSimulateTransactionAccountsConfig { + encoding: Some(UiAccountEncoding::Base64), + addresses: vec![token_account.to_string()], + }), + ..RpcSimulateTransactionConfig::default() + }, + ) + .await + .unwrap(); + if simulation.value.err.is_some() { + for line in simulation.value.logs.iter().flatten() { + eprintln!("{line}"); + } + } + assert_eq!(simulation.value.err, None, "swap simulation should succeed"); + let account_data = simulation + .value + .accounts + .unwrap() + .into_iter() + .next() + .flatten() + .and_then(|account| account.data.decode()) + .expect("simulation should return the requested token account"); + read_u64(&account_data, TOKEN_AMOUNT_OFFSET) +} + +async fn fund_user(rpc: &RpcClient, fixture: &GraduationFixture) { + cheatcode( + rpc, + "surfnet_setAccount", + serde_json::json!([fixture.user.to_string(), { "lamports": 2_000_000_000u64 }]), + ) + .await; + cheatcode( + rpc, + "surfnet_setTokenAccount", + serde_json::json!([fixture.user.to_string(), WSOL.to_string(), { "amount": 1_500_000_000u64 }, null]), + ) + .await; + // buy_v2 requires the user's Token-2022 base ATA to exist. + cheatcode( + rpc, + "surfnet_setTokenAccount", + serde_json::json!([fixture.user.to_string(), fixture.mint.to_string(), { "amount": 0u64 }, TOKEN_2022.to_string()]), + ) + .await; +} + +fn build_buy_v2(fixture: &GraduationFixture, completing_buy_amount: u64) -> Instruction { + let accounts = vec![ + account_meta(PUMP_GLOBAL, false, false), // 0 global + account_meta(fixture.mint, false, false), // 1 base_mint + account_meta(WSOL, false, false), // 2 quote_mint + account_meta(TOKEN_2022, false, false), // 3 base_token_program + account_meta(TOKENKEG, false, false), // 4 quote_token_program + account_meta(ATA_PROGRAM, false, false), // 5 associated_token_program + account_meta(fixture.fee_recipient, false, true), // 6 fee_recipient + account_meta(fixture.fee_recipient_quote, false, true), // 7 associated_quote_fee_recipient + account_meta(fixture.buyback_recipient, false, true), // 8 buyback_fee_recipient + account_meta(fixture.buyback_recipient_quote, false, true), // 9 associated_quote_buyback_fee_recipient + account_meta(fixture.curve, false, true), // 10 bonding_curve + account_meta(fixture.base_vault, false, true), // 11 associated_base_bonding_curve + account_meta(fixture.quote_vault, false, true), // 12 associated_quote_bonding_curve + account_meta(fixture.user, true, true), // 13 user + account_meta(fixture.user_base, false, true), // 14 associated_base_user + account_meta(fixture.user_quote, false, true), // 15 associated_quote_user + account_meta(fixture.creator_vault, false, true), // 16 creator_vault + account_meta(fixture.creator_vault_quote, false, true), // 17 associated_creator_vault + account_meta(fixture.sharing_config, false, false), // 18 sharing_config + account_meta(fixture.global_volume_accumulator, false, false), // 19 global_volume_accumulator + account_meta(fixture.user_volume_accumulator, false, true), // 20 user_volume_accumulator + account_meta(fixture.user_volume_accumulator_quote, false, true), // 21 associated_user_volume_accumulator + account_meta(fixture.fee_config, false, false), // 22 fee_config + account_meta(FEE_PROGRAM, false, false), // 23 fee_program + account_meta(SYSTEM_PROGRAM, false, false), // 24 system_program + account_meta(fixture.pump_event_authority, false, false), // 25 event_authority + account_meta(PUMP, false, false), // 26 program + ]; + let mut data = BUY_V2_DISCRIMINATOR.to_vec(); + data.extend_from_slice(&completing_buy_amount.to_le_bytes()); + data.extend_from_slice(&MAX_SOL_COST.to_le_bytes()); + + Instruction { + program_id: PUMP, + accounts, + data, + } +} + +fn build_migrate_v2(fixture: &GraduationFixture) -> Vec { + let accounts = vec![ + account_meta(PUMP_GLOBAL, false, false), // 0 global + account_meta(fixture.withdraw_authority, false, true), // 1 withdraw_authority + account_meta(fixture.mint, false, false), // 2 base_mint + account_meta(WSOL, false, false), // 3 quote_mint + account_meta(fixture.curve, false, true), // 4 bonding_curve + account_meta(fixture.base_vault, false, true), // 5 associated_base_bonding_curve + account_meta(fixture.quote_vault, false, true), // 6 associated_quote_bonding_curve + account_meta(fixture.user, true, false), // 7 user + account_meta(SYSTEM_PROGRAM, false, false), // 8 system_program + account_meta(PAMM, false, false), // 9 pump_amm_program + account_meta(fixture.pool, false, true), // 10 pool + account_meta(fixture.pool_authority, false, true), // 11 pool_authority + account_meta(fixture.pool_authority_base, false, true), // 12 pool_authority_mint_account + account_meta(fixture.pool_authority_quote, false, true), // 13 pool_authority_quote_account + account_meta(AMM_GLOBAL_CONFIG, false, false), // 14 amm_global_config + account_meta(fixture.lp_mint, false, true), // 15 pool_lp_mint + account_meta(fixture.pool_authority_lp, false, true), // 16 user_pool_token_account + account_meta(fixture.pool_base, false, true), // 17 pool_base_token_account + account_meta(fixture.pool_quote, false, true), // 18 pool_quote_token_account + account_meta(TOKEN_2022, false, false), // 19 base_token_program + account_meta(TOKENKEG, false, false), // 20 quote_token_program + account_meta(TOKEN_2022, false, false), // 21 token_2022_program + account_meta(ATA_PROGRAM, false, false), // 22 associated_token_program + account_meta(fixture.pamm_event_authority, false, false), // 23 pump_amm_event_authority + account_meta(RENT_SYSVAR, false, false), // 24 rent + account_meta(fixture.pump_event_authority, false, false), // 25 event_authority + account_meta(PUMP, false, false), // 26 program + // Remaining accounts feed the pAMM init_boost CPI: the pool's boost vault + // authority PDA and its quote ATA (per the pump-amm IDL). + account_meta(fixture.boost_vault_authority, false, true), // 27 boost_vault_authority + account_meta(fixture.boost_vault, false, true), // 28 boost_vault + ]; + + vec![ + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + Instruction { + program_id: PUMP, + accounts, + data: MIGRATE_V2_DISCRIMINATOR.to_vec(), + }, + ] +} + +/// The fork freezes the discovered coin's accounts on first read while the pump and pAMM +/// programs run live from mainnet. +#[tokio::test(flavor = "multi_thread")] +async fn test_pump_token2022_graduation_lifecycle() { + let (rpc, locker, _runloop) = start_live_surfnet(); + let (mint, preparation) = find_live_graduation_candidate(&rpc).await; + let completing_buy_amount = preparation.completing_buy_amount; + let migration_reserve = preparation.migration_reserve; + + // The builder must derive its plan from the supplied curve state, not from + // constants: a doubled virtual quote reserve must change the finishing buy. + let curve_account = rpc + .get_account(&preparation.addresses.bonding_curve) + .await + .unwrap(); + let mut scaled_curve = curve_account.clone(); + let virtual_quote = read_u64(&scaled_curve.data, CURVE_VIRTUAL_QUOTE_RESERVES_OFFSET); + scaled_curve.data[CURVE_VIRTUAL_QUOTE_RESERVES_OFFSET..CURVE_VIRTUAL_QUOTE_RESERVES_OFFSET + 8] + .copy_from_slice(&(virtual_quote * 2).to_le_bytes()); + let scaled = build_pump_graduation_scenario( + mint, + &rpc.get_account(&mint).await.unwrap(), + &scaled_curve, + &rpc.get_account(&preparation.addresses.curve_vault) + .await + .unwrap(), + None, + &rpc.get_account(&PUMP_GLOBAL).await.unwrap(), + ) + .unwrap(); + assert_ne!( + scaled.completing_buy_amount, completing_buy_amount, + "the graduation plan must be driven by the supplied curve state" + ); + + let user = Keypair::new(); + let global_data = rpc.get_account(&PUMP_GLOBAL).await.unwrap().data; + let fixture = GraduationFixture::new(user.pubkey(), mint, &curve_account.data, &global_data); + + locker + .register_scenario(preparation.scenario, Some(0)) + .unwrap(); + locker + .materialize_overrides_for_slot(&None, 1) + .await + .unwrap(); + fund_user(&rpc, &fixture).await; + + send_transaction( + &rpc, + &user, + vec![build_buy_v2(&fixture, completing_buy_amount)], + ) + .await; + + let curve_after = rpc.get_account(&fixture.curve).await.unwrap(); + assert_eq!( + read_u64(&curve_after.data, CURVE_REAL_TOKEN_RESERVES_OFFSET), + 0, + "buy should exhaust the curve's real token reserves" + ); + assert_eq!( + curve_after.data[CURVE_COMPLETE_OFFSET], 1, + "buy should complete the curve" + ); + assert_eq!( + token_amount(&rpc, &fixture.user_base).await, + completing_buy_amount, + "user should receive the purchased base tokens" + ); + assert_eq!( + token_amount(&rpc, &fixture.base_vault).await, + migration_reserve, + "buy should leave the migration reserve in the curve vault" + ); + + send_transaction(&rpc, &user, build_migrate_v2(&fixture)).await; + + assert_eq!( + rpc.get_account(&fixture.pool).await.unwrap().owner, + PAMM, + "migrate should create a pAMM-owned pool" + ); + assert_eq!( + rpc.get_account(&fixture.lp_mint).await.unwrap().owner, + TOKEN_2022, + "migrate should create a Token-2022 LP mint" + ); + assert_eq!( + token_amount(&rpc, &fixture.pool_base).await, + migration_reserve, + "migrate should seed the pool with the reserved base liquidity" + ); + assert!( + token_amount(&rpc, &fixture.pool_quote).await > 0, + "migrate should seed the pool with quote liquidity" + ); +} + +/// Sell against an established canonical pool; newly migrated pools use a different +/// fee wiring (an open follow-up). +async fn build_sell( + rpc: &RpcClient, + user: Pubkey, + base_mint: Pubkey, + base_token_program: Pubkey, + pool: Pubkey, + base_amount_in: u64, +) -> Instruction { + let global_config = rpc + .get_account(&AMM_GLOBAL_CONFIG) + .await + .expect("AMM global config should load"); + let protocol_fee_recipient = Pubkey::try_from( + &global_config.data + [AMM_PROTOCOL_FEE_RECIPIENTS_OFFSET..AMM_PROTOCOL_FEE_RECIPIENTS_OFFSET + 32], + ) + .unwrap(); + let pool_data = rpc + .get_account(&pool) + .await + .expect("live pool should exist") + .data; + let coin_creator = + Pubkey::try_from(&pool_data[POOL_COIN_CREATOR_OFFSET..POOL_COIN_CREATOR_OFFSET + 32]) + .unwrap(); + let coin_creator_vault_authority = + Pubkey::find_program_address(&[b"creator_vault", coin_creator.as_ref()], &PAMM).0; + let sell_fee_config = + Pubkey::find_program_address(&[b"fee_config", PAMM.as_ref()], &FEE_PROGRAM).0; + let pool_v2 = Pubkey::find_program_address(&[b"pool-v2", base_mint.as_ref()], &PAMM).0; + let breaking_fee_recipient = + Pubkey::from_str_const("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"); + let pamm_event_authority = Pubkey::find_program_address(&[b"__event_authority"], &PAMM).0; + let accounts = vec![ + account_meta(pool, false, true), // 0 pool + account_meta(user, true, true), // 1 user + account_meta(AMM_GLOBAL_CONFIG, false, false), // 2 global_config + account_meta(base_mint, false, false), // 3 base_mint + account_meta(WSOL, false, false), // 4 quote_mint + account_meta( + associated_token_address(&user, &base_mint, &base_token_program), + false, + true, + ), // 5 user_base_token_account + account_meta( + associated_token_address(&user, &WSOL, &TOKENKEG), + false, + true, + ), // 6 user_quote_token_account + account_meta( + associated_token_address(&pool, &base_mint, &base_token_program), + false, + true, + ), // 7 pool_base_token_account + account_meta( + associated_token_address(&pool, &WSOL, &TOKENKEG), + false, + true, + ), // 8 pool_quote_token_account + account_meta(protocol_fee_recipient, false, false), // 9 protocol_fee_recipient + account_meta( + associated_token_address(&protocol_fee_recipient, &WSOL, &TOKENKEG), + false, + true, + ), // 10 protocol_fee_recipient_token_account + account_meta(base_token_program, false, false), // 11 base_token_program + account_meta(TOKENKEG, false, false), // 12 quote_token_program + account_meta(SYSTEM_PROGRAM, false, false), // 13 system_program + account_meta(ATA_PROGRAM, false, false), // 14 associated_token_program + account_meta(pamm_event_authority, false, false), // 15 event_authority + account_meta(PAMM, false, false), // 16 program + account_meta( + associated_token_address(&coin_creator_vault_authority, &WSOL, &TOKENKEG), + false, + true, + ), // 17 coin_creator_vault_ata + account_meta(coin_creator_vault_authority, false, false), // 18 coin_creator_vault_authority + account_meta(sell_fee_config, false, false), // 19 fee_config + account_meta(FEE_PROGRAM, false, false), // 20 fee_program + account_meta(pool_v2, false, false), // 21 pool_v2 + account_meta(breaking_fee_recipient, false, false), // 22 fee_recipient + account_meta( + associated_token_address(&breaking_fee_recipient, &WSOL, &TOKENKEG), + false, + true, + ), // 23 fee_recipient_token_account + ]; + let mut data = SELL_DISCRIMINATOR.to_vec(); + data.extend_from_slice(&base_amount_in.to_le_bytes()); + // Zero slippage protection is acceptable only in this regression test. + data.extend_from_slice(&0u64.to_le_bytes()); + + Instruction { + program_id: PAMM, + accounts, + data, + } +} + +/// The price-shock template must change what the deployed AMM actually quotes, proven on the +/// established live canonical pool (VERIFY-12 / MODEL-06). +#[tokio::test(flavor = "multi_thread")] +async fn price_shock_changes_a_live_pool_swap() { + const LLS_MINT: Pubkey = Pubkey::from_str_const("7LSsEoJGhLeZzGvDofTdNg7M3JttxQqGWNLo6vWMpump"); + + let (rpc, locker, _runloop) = start_live_surfnet(); + let user = Keypair::new(); + let base_token_program = rpc.get_account(&LLS_MINT).await.unwrap().owner; + + cheatcode( + &rpc, + "surfnet_setAccount", + serde_json::json!([user.pubkey().to_string(), { "lamports": 2_000_000_000u64 }]), + ) + .await; + cheatcode( + &rpc, + "surfnet_setTokenAccount", + serde_json::json!([user.pubkey().to_string(), WSOL.to_string(), { "amount": 1_000_000_000u64 }, null]), + ) + .await; + cheatcode( + &rpc, + "surfnet_setTokenAccount", + serde_json::json!([ + user.pubkey().to_string(), + LLS_MINT.to_string(), + { "amount": 1_000_000_000u64 }, + base_token_program.to_string() + ]), + ) + .await; + + let user_quote = associated_token_address(&user.pubkey(), &WSOL, &TOKENKEG); + let pool_quote_balance = + token_amount(&rpc, &associated_token_address(&AMM_POOL, &WSOL, &TOKENKEG)).await; + let sell = build_sell( + &rpc, + user.pubkey(), + LLS_MINT, + base_token_program, + AMM_POOL, + 500_000_000, + ) + .await; + + let baseline = + simulate_token_amount_after_transaction(&rpc, &user, sell.clone(), user_quote).await; + + let registry = TemplateRegistry::new(); + let template = registry + .get("pump-amm-canonical-pool") + .expect("PumpSwap template"); + let values = HashMap::from([ + ( + "base_mint".to_string(), + serde_json::json!(LLS_MINT.to_string()), + ), + ( + "virtual_quote_reserves".to_string(), + serde_json::json!(pool_quote_balance.checked_mul(9).unwrap()), + ), + ]); + let mut pool_override = OverrideInstance::new(template.id.clone(), 0, template.address.clone()) + .with_values(values) + .with_label("PumpSwap virtual quote reserve shock".to_string()); + pool_override.fetch_before_use = true; + let mut price_shock = Scenario::new( + "PumpSwap Price Shock".to_string(), + "Shift a canonical PumpSwap pool price through its virtual quote reserves.".to_string(), + ); + price_shock.tags = vec!["pumpswap".to_string(), "price-shock".to_string()]; + price_shock.add_override(pool_override); + locker.register_scenario(price_shock, Some(100)).unwrap(); + locker + .materialize_overrides_for_slot(&None, 100) + .await + .unwrap(); + + let shocked = + simulate_token_amount_after_transaction(&rpc, &user, sell.clone(), user_quote).await; + assert_ne!( + shocked, baseline, + "the price-shock scenario should change the real swap output" + ); + + send_transaction(&rpc, &user, vec![sell]).await; + assert!( + token_amount(&rpc, &user_quote).await > 0, + "sell should credit the user's quote tokens" + ); +} diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index ffa6477b1..48d560020 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -1116,6 +1116,20 @@ impl TokenAccount { } } + pub fn pack_into_preserving_extensions(&self, original: &[u8]) -> SurfpoolResult> { + let base_len = spl_token_interface::state::Account::LEN; + if original.len() < base_len { + return Err(SurfpoolError::unpack_token_account()); + } + + let mut data = original.to_vec(); + match self { + Self::SplToken2022(account) => account.pack_into_slice(&mut data[..base_len]), + Self::SplToken(account) => account.pack_into_slice(&mut data[..base_len]), + } + Ok(data) + } + pub fn owner(&self) -> Pubkey { match self { Self::SplToken2022(account) => account.owner, @@ -1207,6 +1221,76 @@ impl TokenAccount { } } +#[cfg(test)] +mod token_account_packing_tests { + use super::*; + + #[test] + fn packing_preserves_token_2022_extension_bytes() { + let mut token_account = TokenAccount::new( + &spl_token_2022_interface::id(), + Pubkey::new_unique(), + Pubkey::new_unique(), + None, + ); + token_account.set_amount(42); + let mut original = vec![0u8; 170]; + original[165..].copy_from_slice(&[1, 2, 3, 4, 5]); + + let packed = token_account + .pack_into_preserving_extensions(&original) + .unwrap(); + + assert_eq!(packed.len(), 170); + assert_eq!(&packed[64..72], &42u64.to_le_bytes()); + assert_eq!(&packed[165..], &[1, 2, 3, 4, 5]); + } + + #[test] + fn packing_keeps_classic_token_accounts_at_165_bytes() { + let mut token_account = TokenAccount::new( + &spl_token_interface::id(), + Pubkey::new_unique(), + Pubkey::new_unique(), + None, + ); + token_account.set_amount(42); + + let packed = token_account + .pack_into_preserving_extensions(&[0u8; 165]) + .unwrap(); + + assert_eq!(packed.len(), 165); + assert_eq!(&packed[64..72], &42u64.to_le_bytes()); + } + + #[test] + fn amount_only_repack_changes_only_the_amount_bytes() { + for token_program in [spl_token_interface::id(), spl_token_2022_interface::id()] { + let mut token_account = TokenAccount::new( + &token_program, + Pubkey::new_unique(), + Pubkey::new_unique(), + None, + ); + let mut original = token_account.pack_into_vec(); + if token_program == spl_token_2022_interface::id() { + original.extend_from_slice(&[1, 2, 3, 4, 5]); + } + token_account.set_amount(42); + + let patched = token_account + .pack_into_preserving_extensions(&original) + .unwrap(); + + assert_eq!(patched.len(), original.len()); + assert_eq!(&patched[64..72], &42u64.to_le_bytes()); + assert_eq!(&patched[..64], &original[..64]); + assert_eq!(&patched[72..], &original[72..]); + } + } +} + /// Returns `true` if the given account bytes are a Token-2022 mint that carries /// the transfer-fee config extension. Used to decide whether a fabricated /// confidential account also needs the companion confidential-transfer diff --git a/crates/mcp/src/surfpool/mod.rs b/crates/mcp/src/surfpool/mod.rs index b7568cc95..7ce4c3e08 100644 --- a/crates/mcp/src/surfpool/mod.rs +++ b/crates/mcp/src/surfpool/mod.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + str::FromStr, sync::{Arc, RwLock}, }; @@ -13,10 +14,22 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; use set_token_account::{SeededAccount, SetAccountSuccess, SetTokenAccountsResponse}; +use solana_pubkey::Pubkey; use start_surfnet::StartSurfnetResponse; -use surfpool_core::scenarios::TemplateRegistry; +use surfpool_core::{ + scenarios::{ + TemplateRegistry, + protocols::pump::v1::graduation_builder::{ + build_pump_graduation_scenario, pump_graduation_addresses, + }, + }, + solana_account::Account, + solana_commitment_config::CommitmentConfig, + surfnet::remote::SurfnetRemoteClient, +}; use surfpool_types::{ - CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED, Scenario, VERIFIED_TOKENS_BY_SYMBOL, + CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED, DEFAULT_RPC_PORT, Scenario, + VERIFIED_TOKENS_BY_SYMBOL, }; use crate::helpers::find_next_available_surfnet_port; @@ -104,6 +117,19 @@ pub struct SearchConstantOptionsParams { pub query: String, } +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreatePumpGraduationScenarioParams { + #[schemars( + description = "Live Token-2022 Pump mint. If validation fails, report the error and do not retry without tokenMint." + )] + pub token_mint: String, + #[schemars( + description = "The port of the target running local surfnet instance (e.g., 8899, 18899, 28899, etc.). Omit to use the default port, 8899." + )] + pub surfnet_port: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct StartSurfnetWithTokenAccountsParams { #[schemars( @@ -306,6 +332,13 @@ impl RegisterScenarioResponse { } } +fn scenario_tool_error(message: String) -> CallToolResult { + let response = RegisterScenarioResponse::error(message); + CallToolResult::success(vec![Content::text( + serde_json::to_string(&response).unwrap_or_default(), + )]) +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct GetTokenAddressParams { #[schemars(description = "The token symbol to look up (e.g., 'USDC', 'SOL', 'JUP')")] @@ -345,6 +378,97 @@ impl TokenAddressResponse { } } +impl Surfpool { + /// Reads through the surfnet's own RPC: local state wins, only missing + /// accounts fall back to its remote source. + async fn fetch_surfnet_accounts( + &self, + surfnet_port: Option, + pubkeys: &[Pubkey], + ) -> Result>, String> { + let port = surfnet_port.unwrap_or(DEFAULT_RPC_PORT); + let client = SurfnetRemoteClient::new(format!("http://127.0.0.1:{port}")); + let accounts = client + .get_multiple_accounts(pubkeys, CommitmentConfig::confirmed()) + .await + .map_err(|error| format!("Failed to read accounts from Surfnet: {error}"))?; + + Ok(accounts + .into_iter() + .map(|result| result.map_account().ok()) + .collect()) + } + + async fn stage_scenario(&self, scenario: Scenario) -> Result { + let endpoint = format!( + "http://127.0.0.1:{}/v1/scenarios", + CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED + ); + let response = match reqwest::Client::new() + .post(&endpoint) + .header("Content-Type", "application/json") + .json(&scenario) + .send() + .await + { + Ok(response) => response, + Err(error) => { + let response = RegisterScenarioResponse::error(format!( + "Failed to load scenarios at {endpoint}: {error}" + )); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); + } + }; + let status = response.status(); + let body = match response.text().await { + Ok(body) => body, + Err(error) => { + let response = RegisterScenarioResponse::error(format!( + "Failed to read response text: {error}" + )); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); + } + }; + let response: serde_json::Value = match serde_json::from_str(&body) { + Ok(response) => response, + Err(error) => { + let response = RegisterScenarioResponse::error(format!( + "Failed to parse JSON response: {error}. Response: {body}" + )); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); + } + }; + if status == reqwest::StatusCode::CONFLICT { + let response = RegisterScenarioResponse::error(format!( + "A different scenario is already stored under id {:?}. Pick another id, or delete the existing one first.", + scenario.id + )); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); + } + if let Some(error) = response.get("error") { + let response = RegisterScenarioResponse::error(format!("RPC error: {error}")); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); + } + + let scenario_id = response + .get("id") + .and_then(|value| value.as_str()) + .unwrap_or(&scenario.id); + let url = format!( + "http://127.0.0.1:{}/scenarios?id={scenario_id}&tab=editor", + CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED + ); + let response = RegisterScenarioResponse::success(url); + let json = serde_json::to_string(&response).unwrap_or_default(); + Ok(CallToolResult::success(vec![Content::text(json)])) + } +} + #[tool_router] impl Surfpool { /// Returns a command to start a new local Solana network (surfnet). @@ -724,10 +848,12 @@ impl Surfpool { // Check if the value exists in values map if let Some(value) = override_instance.values.get(&prop.path) { if let Some(value_str) = value.as_str() { - // Validate the value is one of the valid options - let is_valid = constant_def.options.iter().any(|opt| { - opt.value.to_lowercase() == value_str.to_lowercase() - }); + // Base58 is case-sensitive: a case-folded match would + // accept a value that derives a different PDA. + let is_valid = constant_def + .options + .iter() + .any(|opt| opt.value == value_str); if !is_valid { // Show only first 10 options to avoid overwhelming error messages let sample_options: Vec = constant_def @@ -755,6 +881,15 @@ impl Surfpool { sample_options.join("\n ") )); } + } else { + validation_errors.push(format!( + "Override '{}' (template '{}'): Value for '{}' (constant: '{}') must be a base-58 mint address string, got: {}", + override_instance.id, + override_instance.template_id, + prop.path, + constant_name, + value + )); } } else { // Value is missing - required for PDA derivation @@ -802,85 +937,68 @@ impl Surfpool { return Ok(CallToolResult::success(vec![Content::text(json_str)])); } - let load_scenarios_endpoint = format!( - "http://127.0.0.1:{}/v1/scenarios", - CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED - ); - let payload = serde_json::json!(scenario); + self.stage_scenario(scenario).await + } - let client = reqwest::Client::new(); - let http_response = match client - .post(&load_scenarios_endpoint) - .header("Content-Type", "application/json") - .json(&payload) - .send() + #[tool( + description = "Creates an editable Pump Graduation state-preparation scenario for the required tokenMint. If validation fails, report that error and do not retry. The backend validates the mint, incomplete bonding curve, curve vault, and absent canonical PumpSwap pool." + )] + async fn create_pump_graduation_scenario( + &self, + Parameters(params): Parameters, + ) -> Result { + let token_mint = match Pubkey::from_str(params.token_mint.trim()) { + Ok(token_mint) => token_mint, + Err(error) => return Ok(scenario_tool_error(format!("Invalid token mint: {error}"))), + }; + let addresses = match pump_graduation_addresses(&token_mint) { + Ok(addresses) => addresses, + Err(error) => return Ok(scenario_tool_error(error.to_string())), + }; + let pubkeys = [ + token_mint, + addresses.bonding_curve, + addresses.curve_vault, + addresses.canonical_pool, + addresses.global, + ]; + let accounts = match self + .fetch_surfnet_accounts(params.surfnet_port, &pubkeys) .await { - Ok(resp) => resp, - Err(e) => { - let response = RegisterScenarioResponse::error(format!( - "Failed to load scenarios at {}: {}", - load_scenarios_endpoint, e - )); - let json_str = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json_str)])); - } + Ok(accounts) => accounts, + Err(error) => return Ok(scenario_tool_error(error)), }; - - let status = http_response.status(); - - let response_text = match http_response.text().await { - Ok(text) => text, - Err(e) => { - let response = - RegisterScenarioResponse::error(format!("Failed to read response text: {}", e)); - let json_str = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json_str)])); - } + let required = |index: usize, name: &str| { + accounts[index] + .as_ref() + .ok_or_else(|| format!("Pump {name} account not found")) }; - - let rpc_response: serde_json::Value = match serde_json::from_str(&response_text) { - Ok(json) => json, - Err(e) => { - let response = RegisterScenarioResponse::error(format!( - "Failed to parse JSON response: {}. Response: {}", - e, response_text - )); - let json_str = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json_str)])); - } + let preparation = match build_pump_graduation_scenario( + token_mint, + match required(0, "mint") { + Ok(account) => account, + Err(error) => return Ok(scenario_tool_error(error)), + }, + match required(1, "bonding curve") { + Ok(account) => account, + Err(error) => return Ok(scenario_tool_error(error)), + }, + match required(2, "curve vault") { + Ok(account) => account, + Err(error) => return Ok(scenario_tool_error(error)), + }, + accounts[3].as_ref(), + match required(4, "global") { + Ok(account) => account, + Err(error) => return Ok(scenario_tool_error(error)), + }, + ) { + Ok(preparation) => preparation, + Err(error) => return Ok(scenario_tool_error(error.to_string())), }; - // A different scenario already occupies this id: say so instead of - // reporting a success the model would trust - if status == reqwest::StatusCode::CONFLICT { - let response = RegisterScenarioResponse::error(format!( - "A different scenario is already stored under id {:?}. Pick another id, or delete the existing one first.", - scenario.id - )); - let json_str = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json_str)])); - } - - if let Some(error) = rpc_response.get("error") { - let response = RegisterScenarioResponse::error(format!("RPC error: {}", error)); - let json_str = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json_str)])); - } - - // Extract the scenario id from the response - let scenario_id = rpc_response - .get("id") - .and_then(|v| v.as_str()) - .unwrap_or(&scenario.id); - - let url = format!( - "http://127.0.0.1:{}/scenarios?id={}&tab=editor", - CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED, scenario_id - ); - let response = RegisterScenarioResponse::success(url); - let json_str = serde_json::to_string(&response).unwrap_or_default(); - Ok(CallToolResult::success(vec![Content::text(json_str)])) + self.stage_scenario(preparation.scenario).await } #[tool( @@ -1153,6 +1271,33 @@ mod tests { assert_eq!(parsed.template_id, "pyth-price-feed-v2"); } + #[test] + fn pump_graduation_mint_is_required() { + assert!( + serde_json::from_value::(serde_json::json!({})) + .is_err() + ); + } + + #[tokio::test] + async fn pump_graduation_rejects_invalid_inputs_before_rpc() { + let surfpool = Surfpool::new(); + + let graduation = surfpool + .create_pump_graduation_scenario(Parameters(CreatePumpGraduationScenarioParams { + token_mint: "not-a-mint".to_string(), + surfnet_port: None, + })) + .await + .expect("tool result"); + assert!( + json_of(&graduation)["error"] + .as_str() + .expect("error") + .contains("Invalid token mint") + ); + } + fn json_of(result: &CallToolResult) -> serde_json::Value { let text = &result.content[0].as_text().expect("text content").text; serde_json::from_str(text).expect("valid JSON payload") @@ -1273,4 +1418,223 @@ mod tests { .unwrap(); assert_eq!(unknown_constant.is_error, Some(true)); } + + #[tokio::test] + async fn get_override_templates_lists_the_pump_templates_compactly() { + let surfpool = Surfpool::new(); + let result = surfpool.get_override_templates().await.unwrap(); + assert_ne!(result.is_error, Some(true)); + + let templates = json_of(&result); + let templates = templates.as_array().unwrap(); + for id in [ + "pump-bonding-curve-custom", + "pump-global", + "pump-amm-pool-state", + "pump-amm-canonical-pool", + "pump-amm-global-config", + ] { + let template = templates + .iter() + .find(|t| t["id"] == id) + .unwrap_or_else(|| panic!("template {id} missing from the model's view")); + assert!( + template.get("idl").is_none(), + "{id} must not inline the ~160KB IDL into the LLM context" + ); + } + + let curve = templates + .iter() + .find(|t| t["id"] == "pump-bonding-curve-custom") + .unwrap(); + let token_mint = &curve["constants"]["token_mint"]; + assert!( + token_mint["optionsCount"].as_u64().unwrap() > 0, + "the verified-tokens catalog must be visible as a summary" + ); + assert!(token_mint.get("options").is_none()); + } + + #[tokio::test] + async fn search_resolves_pump_coin_mints_for_both_programs() { + let surfpool = Surfpool::new(); + + let result = surfpool + .search_constant_options(search("pump-bonding-curve-custom", None, "pump")) + .await + .unwrap(); + assert_ne!(result.is_error, Some(true)); + let payload = json_of(&result); + let results = payload["results"].as_array().unwrap(); + assert!(!results.is_empty(), "a pump coin must be findable"); + assert!( + results + .iter() + .all(|r| !r["value"].as_str().unwrap().is_empty()), + "every result must carry the mint create_scenario expects" + ); + assert!( + results + .iter() + .any(|r| r["value"].as_str().unwrap().ends_with("pump")), + "pump.fun mints are recognizable by their suffix" + ); + + let result = surfpool + .search_constant_options(search("pump-amm-canonical-pool", Some("token_mint"), "")) + .await + .unwrap(); + assert_ne!(result.is_error, Some(true)); + let payload = json_of(&result); + assert!( + payload["totalMatches"].as_u64().unwrap() > 0, + "the canonical pool template must expose base mints to search" + ); + } + + #[tokio::test] + async fn pump_token_catalogs_offer_only_pump_mints() { + let registry = TemplateRegistry::new(); + for template_id in ["pump-bonding-curve-custom", "pump-amm-canonical-pool"] { + let template = registry.get(template_id).expect("template"); + let constant = template.constants.get("token_mint").expect("constant"); + assert_eq!( + constant.options.len(), + 976, + "{template_id} must offer every pump-suffixed catalog mint (976 in the CSV)" + ); + assert!( + constant.options.iter().all(|o| o.value.ends_with("pump")), + "{template_id} must offer only pump.fun mints" + ); + } + + let surfpool = Surfpool::new(); + for mint in [ + "So11111111111111111111111111111111111111112", + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + ] { + let result = surfpool + .search_constant_options(search("pump-bonding-curve-custom", None, mint)) + .await + .unwrap(); + let payload = json_of(&result); + assert_eq!( + payload["totalMatches"].as_u64().unwrap(), + 0, + "{mint} must not be offered for a bonding curve" + ); + } + } + + #[tokio::test] + async fn create_scenario_rejects_bad_pump_overrides_before_any_http_call() { + let surfpool = Surfpool::new(); + let registry = TemplateRegistry::new(); + let curve_address = registry + .get("pump-bonding-curve-custom") + .expect("template") + .address + .clone(); + + let mut unknown = surfpool_types::Scenario::new( + "bad template".to_string(), + "unknown templateId must be rejected".to_string(), + ); + unknown.add_override(surfpool_types::OverrideInstance::new( + "pump-bonding-curve".to_string(), + 0, + curve_address.clone(), + )); + let result = surfpool.create_scenario(Parameters(unknown)).await.unwrap(); + let text = &result.content[0].as_text().expect("text").text; + assert!( + text.contains("Invalid templateId"), + "a misremembered id must be named, got: {text}" + ); + + let mut incomplete = surfpool_types::Scenario::new( + "missing mint".to_string(), + "a PDA constant without a value must be rejected".to_string(), + ); + incomplete.add_override( + surfpool_types::OverrideInstance::new( + "pump-bonding-curve-custom".to_string(), + 0, + curve_address, + ) + .with_values(HashMap::from([( + "complete".to_string(), + serde_json::json!(true), + )])), + ); + let result = surfpool + .create_scenario(Parameters(incomplete)) + .await + .unwrap(); + let text = &result.content[0].as_text().expect("text").text; + assert!( + text.contains("Missing required value") && text.contains("token_mint"), + "the missing PDA seed value must be named, got: {text}" + ); + + let mut wrong_type = surfpool_types::Scenario::new( + "numeric mint".to_string(), + "a non-string mint must be rejected, not silently skipped".to_string(), + ); + wrong_type.add_override( + surfpool_types::OverrideInstance::new( + "pump-bonding-curve-custom".to_string(), + 0, + registry + .get("pump-bonding-curve-custom") + .expect("template") + .address + .clone(), + ) + .with_values(HashMap::from([( + "token_mint".to_string(), + serde_json::json!(12345), + )])), + ); + let result = surfpool + .create_scenario(Parameters(wrong_type)) + .await + .unwrap(); + let text = &result.content[0].as_text().expect("text").text; + assert!( + text.contains("must be a base-58 mint address string") && text.contains("token_mint"), + "the wrong-typed mint must be named, got: {text}" + ); + + let mut tampered = surfpool_types::Scenario::new( + "tampered mint".to_string(), + "base58 is case-sensitive; a case-folded match targets another PDA".to_string(), + ); + tampered.add_override( + surfpool_types::OverrideInstance::new( + "pump-bonding-curve-custom".to_string(), + 0, + registry + .get("pump-bonding-curve-custom") + .expect("template") + .address + .clone(), + ) + .with_values(HashMap::from([( + "token_mint".to_string(), + serde_json::json!("9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgPUMP"), + )])), + ); + let result = surfpool + .create_scenario(Parameters(tampered)) + .await + .unwrap(); + let text = &result.content[0].as_text().expect("text").text; + assert!( + text.contains("Invalid value"), + "a case-flipped mint must be rejected, got: {text}" + ); + } } diff --git a/crates/types/src/scenarios.rs b/crates/types/src/scenarios.rs index fb3859572..f328773ec 100644 --- a/crates/types/src/scenarios.rs +++ b/crates/types/src/scenarios.rs @@ -706,6 +706,9 @@ pub enum YamlConstantSource { /// Optional limit on number of tokens to include #[serde(default)] limit: Option, + /// Optional required suffix on the token's mint address (e.g. "pump") + #[serde(default)] + address_suffix: Option, }, } @@ -731,13 +734,14 @@ impl YamlConstantDefinition { source, filter_tags, limit, + address_suffix, } => { if source == "verified_tokens" { - use crate::verified_tokens::VERIFIED_TOKENS_BY_SYMBOL; + use crate::verified_tokens::VERIFIED_TOKENS; - let mut tokens: Vec<_> = VERIFIED_TOKENS_BY_SYMBOL + let mut tokens: Vec<_> = VERIFIED_TOKENS .iter() - .filter(|(_, _token)| { + .filter(|_token| { // If no filter tags specified, include all tokens if filter_tags.is_empty() { return true; @@ -748,8 +752,14 @@ impl YamlConstantDefinition { // TODO: Parse tags from CSV into TokenInfo struct true }) - .map(|(symbol, token)| ConstantOption { - id: symbol.to_lowercase(), + .filter(|token| { + address_suffix + .as_deref() + .map_or(true, |suffix| token.address.ends_with(suffix)) + }) + .map(|token| ConstantOption { + // The mint address: unique even when symbols collide + id: token.address.clone(), label: format!("{} ({})", token.symbol, token.name), description: Some(token.name.clone()), value: token.address.clone(), @@ -774,8 +784,8 @@ impl YamlConstantDefinition { }) .collect(); - // Sort by symbol for consistent ordering - tokens.sort_by(|a, b| a.id.cmp(&b.id)); + // Deterministic order keeps search_constant_options paging stable + tokens.sort_by(|a, b| a.label.cmp(&b.label).then_with(|| a.id.cmp(&b.id))); // Apply limit if specified if let Some(limit) = limit { diff --git a/crates/types/src/verified_tokens.rs b/crates/types/src/verified_tokens.rs index 6ac43e556..b3958e55d 100644 --- a/crates/types/src/verified_tokens.rs +++ b/crates/types/src/verified_tokens.rs @@ -45,9 +45,11 @@ fn parse_csv_line(line: &str) -> Vec { fields } -pub static VERIFIED_TOKENS_BY_SYMBOL: Lazy> = Lazy::new(|| { +/// Every catalog row in CSV order. Option lists must use this: the by-symbol +/// map collapses tokens sharing a symbol and silently drops their mints. +pub static VERIFIED_TOKENS: Lazy> = Lazy::new(|| { let csv = include_str!("verified_tokens.csv"); - let mut map = HashMap::new(); + let mut tokens = Vec::new(); for (i, line) in csv.lines().enumerate() { if i == 0 { @@ -69,18 +71,23 @@ pub static VERIFIED_TOKENS_BY_SYMBOL: Lazy> = Lazy::n let icon = fields[3].clone(); let decimals: u8 = fields[4].parse().unwrap_or(0); - let token = TokenInfo { + tokens.push(TokenInfo { address, name, - symbol: symbol.clone(), + symbol, decimals, logo_uri: if icon.is_empty() { None } else { Some(icon) }, - }; - - map.insert(symbol.to_uppercase(), token); + }); } - map + tokens +}); + +pub static VERIFIED_TOKENS_BY_SYMBOL: Lazy> = Lazy::new(|| { + VERIFIED_TOKENS + .iter() + .map(|token| (token.symbol.to_uppercase(), token.clone())) + .collect() }); #[cfg(test)] From 48b3c39e69b9e04f7541fbc287f478df6a526742 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Tue, 1 Sep 2026 14:53:41 -0400 Subject: [PATCH 02/13] fix(core): enable tls cert verification for datasource (#770) --- crates/core/src/runloops/mod.rs | 12 +++-- crates/core/src/surfnet/remote.rs | 80 +++++++++++-------------------- 2 files changed, 35 insertions(+), 57 deletions(-) diff --git a/crates/core/src/runloops/mod.rs b/crates/core/src/runloops/mod.rs index 0d499ab35..8a59e56ad 100644 --- a/crates/core/src/runloops/mod.rs +++ b/crates/core/src/runloops/mod.rs @@ -186,12 +186,12 @@ pub async fn start_local_surfnet_runloop( let remote_rpc_client = match simnet.offline_mode { true => None, - false => SurfnetRemoteClient::new_unsafe( + false => Some(SurfnetRemoteClient::try_new( simnet .remote_rpc_url .as_ref() .unwrap_or(&DEFAULT_MAINNET_RPC_URL.to_string()), - ), + )?), }; svm_locker.initialize(&remote_rpc_client).await?; @@ -566,8 +566,8 @@ pub async fn start_block_production_runloop( SimnetCommand::FetchRemoteAccounts(pubkeys, remote_url) => { // The submitter already marked RemoteAccounts as started; // StartStartupTask precedes this command on the same channel. - let fetch_result = match SurfnetRemoteClient::new_unsafe(&remote_url) { - Some(remote_client) => match svm_locker + let fetch_result = match SurfnetRemoteClient::try_new(&remote_url) { + Ok(remote_client) => match svm_locker .get_multiple_accounts_with_remote_fallback( &remote_client, &pubkeys, @@ -595,7 +595,9 @@ pub async fn start_block_production_runloop( "Failed to fetch remote accounts {pubkeys:?}: {error}" )), }, - None => Err(format!("Invalid remote RPC URL: {remote_url}")), + Err(error) => Err(format!( + "Unable to initialize remote RPC client: {error}" + )), }; if let Err(error) = &fetch_result { diff --git a/crates/core/src/surfnet/remote.rs b/crates/core/src/surfnet/remote.rs index 89da51283..ed5bc8a15 100644 --- a/crates/core/src/surfnet/remote.rs +++ b/crates/core/src/surfnet/remote.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, str::FromStr, time::Duration}; +use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration}; use async_trait::async_trait; use serde_json::json; @@ -48,13 +48,12 @@ use crate::{ /// How long one call to the datasource gets, start to finish. /// -/// The HTTP client's timeout bounds a single attempt rather than a call: -/// solana's sender retries a 429 up to five times and honours `Retry-After` -/// for as much as 120 seconds each time, so a throttled datasource can hold a -/// call for ten minutes with no individual attempt ever timing out. The value -/// sits comfortably above that 30 second per-attempt timeout, since a deadline -/// at or below it would cut off attempts that were going to succeed. +/// Without this outer deadline, the HTTP timeout applies per attempt, +/// so Solana retry/backoff handling can keep a datasource call alive +/// for up to ten minutes. const DATASOURCE_DEADLINE: Duration = Duration::from_secs(60); +const DATASOURCE_HTTP_TIMEOUT: Duration = Duration::from_secs(30); +const DATASOURCE_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90); fn sanitized_client_error(error: &ClientError, datasource_url: &str) -> String { let endpoint = @@ -155,56 +154,27 @@ struct SurfpoolRpcClient { } impl SurfpoolRpcClient { - fn new(remote_rpc_url: U) -> Self { + fn try_new(remote_rpc_url: U) -> Result { + let client = reqwest::Client::builder() + .default_headers(HttpSender::default_headers()) + .timeout(DATASOURCE_HTTP_TIMEOUT) + .pool_idle_timeout(DATASOURCE_POOL_IDLE_TIMEOUT) + .build()?; let sender = DeadlineSender::new( - HttpSender::new(remote_rpc_url.to_string()), + HttpSender::new_with_client(remote_rpc_url, client), DATASOURCE_DEADLINE, ); let client = RpcClient::new_sender( sender, RpcClientConfig::with_commitment(CommitmentConfig::default()), ); - SurfpoolRpcClient { client } - } - - /// A variant that accepts invalid TLS certificates, for datasources - /// behind self-signed certs. - fn new_unsafe(remote_rpc_url: U) -> Option { - use reqwest; - - // Construction can fail after a fork (the daemonize path), so a - // failure logs and surfaces as None rather than a panic. - let client = match reqwest::Client::builder() - .danger_accept_invalid_certs(true) - .tls_built_in_root_certs(false) - .tls_built_in_webpki_certs(false) - .timeout(std::time::Duration::from_secs(30)) - .build() - { - Ok(client) => client, - Err(e) => { - error!("unable to initialize datasource client: {}", e); - return None; - } - }; - let sender = DeadlineSender::new( - HttpSender::new_with_client(remote_rpc_url, client), - DATASOURCE_DEADLINE, - ); - let client = RpcClient::new_sender(sender, RpcClientConfig::default()); - Some(SurfpoolRpcClient { client }) + Ok(SurfpoolRpcClient { client }) } } +#[derive(Clone)] pub struct SurfnetRemoteClient { - pub client: RpcClient, -} -impl Clone for SurfnetRemoteClient { - fn clone(&self) -> Self { - let remote_rpc_url = self.client.url(); - SurfnetRemoteClient::new_unsafe(remote_rpc_url) - .expect("unable to clone SurfnetRemoteClient") - } + pub client: Arc, } pub trait SomeRemoteCtx { @@ -220,14 +190,12 @@ impl SomeRemoteCtx for Option { impl SurfnetRemoteClient { pub fn new(remote_rpc_url: U) -> Self { - SurfnetRemoteClient { - client: SurfpoolRpcClient::new(remote_rpc_url).client, - } + Self::try_new(remote_rpc_url).expect("unable to initialize datasource client") } - pub fn new_unsafe(remote_rpc_url: U) -> Option { - SurfpoolRpcClient::new_unsafe(remote_rpc_url).map(|rpc_client| SurfnetRemoteClient { - client: rpc_client.client, + pub fn try_new(remote_rpc_url: U) -> Result { + SurfpoolRpcClient::try_new(remote_rpc_url).map(|rpc_client| SurfnetRemoteClient { + client: Arc::new(rpc_client.client), }) } @@ -913,6 +881,14 @@ mod tests { assert_eq!(requests[0].1[1]["commitment"], "confirmed"); } + #[test] + fn cloned_remote_clients_share_the_rpc_client() { + let client = SurfnetRemoteClient::new("http://127.0.0.1:8899"); + let cloned_client = client.clone(); + + assert!(Arc::ptr_eq(&client.client, &cloned_client.client)); + } + /// A call that never completes, whether because the endpoint went quiet /// or because its retry policy never gave control back. The deadline does /// not need to know which. From dd80eca4129814f761361cdf11a412d9b485bcc7 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Wed, 2 Sep 2026 13:00:16 -0400 Subject: [PATCH 03/13] fix(tests): convert RpcClient to expected type in test cases (#791) --- crates/core/src/surfnet/remote.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/core/src/surfnet/remote.rs b/crates/core/src/surfnet/remote.rs index ed5bc8a15..a370b2e8d 100644 --- a/crates/core/src/surfnet/remote.rs +++ b/crates/core/src/surfnet/remote.rs @@ -687,7 +687,8 @@ mod tests { requests: Arc::clone(&requests), }, RpcClientConfig::default(), - ), + ) + .into(), }; let signature = Signature::new_unique(); let config = RpcTransactionConfig { @@ -719,7 +720,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn a_remote_transaction_provider_failure_reaches_the_locker_caller() { let client = SurfnetRemoteClient { - client: RpcClient::new_sender(ReturnsError, RpcClientConfig::default()), + client: RpcClient::new_sender(ReturnsError, RpcClientConfig::default()).into(), }; let signature = Signature::new_unique(); let (svm, _, _) = SurfnetSvm::default(); @@ -864,7 +865,8 @@ mod tests { requests: Arc::clone(&requests), }, RpcClientConfig::default(), - ), + ) + .into(), }; let pubkey = Pubkey::new_unique(); From 8747aab7a651ed1898b6303e4d5722aad9724ee1 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Wed, 2 Sep 2026 13:00:32 -0400 Subject: [PATCH 04/13] fix(core): cache remote genesis hash (#789) --- crates/core/src/surfnet/locker.rs | 143 +++++++++++++++++++++++++-- crates/core/src/surfnet/svm.rs | 4 + crates/core/src/tests/integration.rs | 1 + 3 files changed, 140 insertions(+), 8 deletions(-) diff --git a/crates/core/src/surfnet/locker.rs b/crates/core/src/surfnet/locker.rs index 3b117d831..c6e4de48a 100644 --- a/crates/core/src/surfnet/locker.rs +++ b/crates/core/src/surfnet/locker.rs @@ -260,14 +260,16 @@ impl SurfnetSvmLocker { return Ok(()); }; - let (mut epoch_info, epoch_schedule) = { + let (mut epoch_info, epoch_schedule, some_genesis_hash) = { let epoch_info = remote_client.get_epoch_info().await?; let epoch_schedule = remote_client.get_epoch_schedule().await?; - (epoch_info, epoch_schedule) + let some_genesis_hash = remote_client.get_genesis_hash().await.ok(); + (epoch_info, epoch_schedule, some_genesis_hash) }; epoch_info.transaction_count = None; self.with_svm_writer(move |svm_writer| { + svm_writer.cached_genesis_hash = some_genesis_hash; svm_writer.initialize(epoch_info, epoch_schedule); }); Ok(()) @@ -3816,19 +3818,31 @@ impl SurfnetSvmLocker { } pub fn get_genesis_hash_local(&self) -> SvmAccessContext { - self.with_contextualized_svm_reader(|svm_reader| svm_reader.genesis_config.hash()) + self.with_contextualized_svm_reader(|svm_reader| { + svm_reader + .cached_genesis_hash + .unwrap_or_else(|| svm_reader.genesis_config.hash()) + }) } pub async fn get_genesis_hash( &self, remote_ctx: &Option, ) -> SurfpoolContextualizedResult { - if let Some(client) = remote_ctx { + if self.with_svm_reader(|svm_reader| svm_reader.cached_genesis_hash.is_none()) + && let Some(client) = remote_ctx + { let remote_hash = client.get_genesis_hash().await?; - Ok(self.with_contextualized_svm_reader(|_| remote_hash)) - } else { - Ok(self.get_genesis_hash_local()) + self.with_svm_writer(|svm_writer| { + // Startup normally populates this first. Keep the check so a concurrent + // cache-miss request cannot replace a value that another request just stored. + if svm_writer.cached_genesis_hash.is_none() { + svm_writer.cached_genesis_hash = Some(remote_hash); + } + }); } + + Ok(self.get_genesis_hash_local()) } } @@ -4559,13 +4573,25 @@ pub fn format_ui_amount(amount: u64, decimals: u8) -> Option { #[cfg(test)] mod tests { - use std::collections::HashMap; + use std::{ + collections::HashMap, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + }; + use async_trait::async_trait; use solana_account::Account; use solana_account_decoder::UiAccountEncoding; + use solana_client::{ + nonblocking::rpc_client::RpcClient, rpc_client::RpcClientConfig, rpc_request::RpcRequest, + }; use solana_epoch_schedule::EpochSchedule; use solana_keypair::Keypair; use solana_message::{Message, VersionedMessage}; + use solana_rpc_client::rpc_sender::{RpcSender, RpcTransportStats}; + use solana_rpc_client_api::client_error::Result as ClientResult; use solana_sdk_ids::system_program; use solana_signer::Signer; use solana_system_interface::instruction as system_instruction; @@ -4599,6 +4625,87 @@ mod tests { ] } + struct StartupRpcSender { + genesis_hash: Hash, + requests: Arc, + } + + #[async_trait] + impl RpcSender for StartupRpcSender { + async fn send( + &self, + request: RpcRequest, + _params: serde_json::Value, + ) -> ClientResult { + self.requests.fetch_add(1, Ordering::Relaxed); + + Ok(match request { + RpcRequest::GetEpochInfo => serde_json::json!({ + "epoch": 1, + "slotIndex": 2, + "slotsInEpoch": 432000, + "absoluteSlot": 2, + "blockHeight": 2, + "transactionCount": null, + }), + RpcRequest::GetEpochSchedule => { + serde_json::to_value(EpochSchedule::without_warmup()).unwrap() + } + RpcRequest::GetGenesisHash => serde_json::json!(self.genesis_hash.to_string()), + _ => panic!("unexpected startup RPC request: {request:?}"), + }) + } + + fn get_transport_stats(&self) -> RpcTransportStats { + RpcTransportStats::default() + } + + fn url(&self) -> String { + "http://startup.example".to_string() + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn initialize_fetches_and_caches_genesis_hash() { + let (surfnet_svm, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default(); + let svm_locker = SurfnetSvmLocker::new(surfnet_svm); + let expected_hash = Hash::new_from_array([8; 32]); + let requests = Arc::new(AtomicUsize::new(0)); + let remote_client = SurfnetRemoteClient { + client: RpcClient::new_sender( + StartupRpcSender { + genesis_hash: expected_hash, + requests: Arc::clone(&requests), + }, + RpcClientConfig::default(), + ), + }; + let remote_ctx = Some(remote_client); + + svm_locker + .initialize(&remote_ctx) + .await + .expect("startup RPC calls should succeed"); + + assert_eq!( + svm_locker + .get_genesis_hash(&remote_ctx) + .await + .expect("cached genesis hash should be available") + .inner, + expected_hash + ); + assert_eq!( + svm_locker + .get_genesis_hash(&remote_ctx) + .await + .expect("cached genesis hash should remain available") + .inner, + expected_hash + ); + assert_eq!(requests.load(Ordering::Relaxed), 3); + } + #[cfg(feature = "sqlite")] #[tokio::test(flavor = "multi_thread")] async fn delayed_remote_account_cannot_overwrite_locally_created_account() { @@ -7015,6 +7122,26 @@ mod tests { ); } + #[tokio::test(flavor = "multi_thread")] + async fn get_genesis_hash_uses_cached_hash_when_remote_is_configured() { + let (surfnet_svm, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default(); + let svm_locker = SurfnetSvmLocker::new(surfnet_svm); + let expected_hash = Hash::new_from_array([7; 32]); + + svm_locker.with_svm_writer(|svm_writer| { + svm_writer.cached_genesis_hash = Some(expected_hash); + }); + + // If the cache were ignored, this deliberately unreachable endpoint would be queried. + let remote_client = SurfnetRemoteClient::new("http://127.0.0.1:1"); + let result = svm_locker + .get_genesis_hash(&Some(remote_client)) + .await + .expect("cached genesis hash should not require the remote RPC"); + + assert_eq!(result.inner, expected_hash); + } + #[test] fn test_format_ui_amount_scales_by_decimals() { assert_eq!(format_ui_amount(0, 0), Some(0.0)); diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 6a00a86ba..6508463f5 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -356,6 +356,8 @@ pub struct SurfnetSvm { pub non_circulating_supply: u64, pub non_circulating_accounts: Vec, pub genesis_config: GenesisConfig, + /// Genesis hash fetched from the remote RPC during startup, when configured. + pub cached_genesis_hash: Option, pub inflation: Inflation, /// A global monotonically increasing atomic number, which can be used to tell the order of the account update. /// For example, when an account is updated in the same slot multiple times, @@ -626,6 +628,7 @@ impl SurfnetSvm { non_circulating_supply: self.non_circulating_supply, non_circulating_accounts: self.non_circulating_accounts.clone(), genesis_config: self.genesis_config.clone(), + cached_genesis_hash: self.cached_genesis_hash, inflation: self.inflation, write_version: self.write_version, feature_set: self.feature_set.clone(), @@ -1083,6 +1086,7 @@ impl SurfnetSvm { non_circulating_supply: 0, non_circulating_accounts: Vec::new(), genesis_config: GenesisConfig::default(), + cached_genesis_hash: None, inflation: Inflation::default(), write_version: 0, registered_idls: registered_idls_db, diff --git a/crates/core/src/tests/integration.rs b/crates/core/src/tests/integration.rs index fa182dcc8..cd4e51fbf 100644 --- a/crates/core/src/tests/integration.rs +++ b/crates/core/src/tests/integration.rs @@ -7424,6 +7424,7 @@ async fn test_ws_signature_subscribe_does_not_miss_local_commit_during_remote_lo "firstNormalSlot": 0, }), "getTransaction" => serde_json::Value::Null, + "getGenesisHash" => serde_json::Value::String(Hash::default().to_string()), unexpected => panic!("unexpected datasource method: {unexpected}"), }; let response = serde_json::json!({ From 041bb80700c826e3512537e2171cdea3524ee216 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Wed, 2 Sep 2026 13:00:58 -0400 Subject: [PATCH 05/13] perf(core): add remote tx fetch bypassing (#785) --- crates/core/src/rpc/full.rs | 165 ++++++++++++++++++++++++++- crates/core/src/runloops/mod.rs | 5 +- crates/core/src/surfnet/locker.rs | 68 ++++++++--- crates/core/src/surfnet/svm.rs | 31 +++++ crates/core/src/tests/integration.rs | 10 +- 5 files changed, 256 insertions(+), 23 deletions(-) diff --git a/crates/core/src/rpc/full.rs b/crates/core/src/rpc/full.rs index fa1ec8ac0..4238638c5 100644 --- a/crates/core/src/rpc/full.rs +++ b/crates/core/src/rpc/full.rs @@ -1637,8 +1637,11 @@ impl Full for SurfpoolFullRpc { &self, meta: Self::Metadata, signature_strs: Vec, - _config: Option, + config: Option, ) -> BoxFuture>>>> { + let search_transaction_history = config + .map(|config| config.search_transaction_history) + .unwrap_or(false); let signatures = match signature_strs .iter() .map(|s| { @@ -1658,7 +1661,11 @@ impl Full for SurfpoolFullRpc { Ok(res) => res, Err(e) => return e.into(), }; - let remote_client = remote_ctx.map(|(r, _)| r); + let remote_client = if search_transaction_history { + remote_ctx.map(|(remote_client, _)| remote_client) + } else { + None + }; Box::pin(async move { // Capture the context slot once at the beginning to ensure consistency @@ -1748,7 +1755,9 @@ impl Full for SurfpoolFullRpc { }; let (status_update_tx, status_update_rx) = crossbeam_channel::bounded(1); - ctx.simnet_commands_tx + ctx.svm_locker.mark_transaction_pending(signature); + if ctx + .simnet_commands_tx .send(SimnetCommand::ProcessTransaction( ctx.id, unsanitized_tx, @@ -1756,9 +1765,14 @@ impl Full for SurfpoolFullRpc { config.base.skip_preflight, config.skip_sig_verify, )) - .map_err(|_| RpcCustomError::NodeUnhealthy { + .is_err() + { + ctx.svm_locker.mark_transaction_complete(&signature); + return Err(RpcCustomError::NodeUnhealthy { num_slots_behind: None, - })?; + } + .into()); + } match status_update_rx.recv() { Ok(TransactionStatusEvent::SimulationFailure((error, metadata))) => { @@ -2762,6 +2776,7 @@ mod tests { }; use surfpool_types::{SimnetCommand, TransactionConfirmationStatus}; use test_case::test_case; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; use super::*; use crate::{ @@ -2819,6 +2834,11 @@ mod tests { loop { match mempool_rx.recv() { Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _, _)) => { + let sig = tx.signatures[0]; + assert!( + setup.context.svm_locker.is_transaction_pending(&sig), + "sendTransaction should mark the signature before enqueueing it" + ); let mut writer = setup.context.svm_locker.0.write().await; let slot = writer.get_latest_absolute_slot(); writer.transactions_queued_for_confirmation.push_back(( @@ -2826,7 +2846,6 @@ mod tests { status_tx.clone(), None, )); - let sig = tx.signatures[0]; let tx_with_status_meta = TransactionWithStatusMeta { slot, transaction: tx, @@ -2848,6 +2867,9 @@ mod tests { TransactionConfirmationStatus::Confirmed, )) .unwrap(); + drop(writer); + setup.context.svm_locker.mark_transaction_complete(&sig); + assert!(!setup.context.svm_locker.is_transaction_pending(&sig)); break; } Ok(SimnetCommand::AirdropProcessed) => continue, @@ -3113,6 +3135,137 @@ mod tests { ); } + #[tokio::test(flavor = "multi_thread")] + async fn test_get_signature_statuses_respects_search_transaction_history() { + let missing_signature = Signature::new_unique(); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("test datasource should bind"); + let address = listener + .local_addr() + .expect("test datasource should have an address"); + let datasource_request = tokio::spawn(async move { + let (mut stream, _) = listener + .accept() + .await + .expect("history lookup should reach the datasource"); + let mut request = [0; 4096]; + let bytes_read = stream + .read(&mut request) + .await + .expect("test datasource should read the request"); + let request = String::from_utf8_lossy(&request[..bytes_read]); + assert!(request.contains("getTransaction")); + + let body = r#"{"jsonrpc":"2.0","result":null,"id":0}"#; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}", + body.len(), + body + ); + stream + .write_all(response.as_bytes()) + .await + .expect("test datasource should write the response"); + }); + + let mut setup = TestSetup::new(SurfpoolFullRpc); + setup.context.remote_rpc_client = + Some(SurfnetRemoteClient::new(format!("http://{address}"))); + + for config in [ + None, + Some(RpcSignatureStatusConfig { + search_transaction_history: false, + }), + ] { + let response = setup + .rpc + .get_signature_statuses( + Some(setup.context.clone()), + vec![missing_signature.to_string()], + config, + ) + .await + .expect("recent-status lookups must not query the datasource"); + + assert_eq!(response.value.len(), 1); + assert!(response.value[0].is_none()); + } + + setup + .context + .svm_locker + .mark_transaction_pending(missing_signature); + setup + .context + .svm_locker + .mark_transaction_pending(missing_signature); + + for _ in 0..2 { + let response = setup + .rpc + .get_signature_statuses( + Some(setup.context.clone()), + vec![missing_signature.to_string()], + Some(RpcSignatureStatusConfig { + search_transaction_history: true, + }), + ) + .await + .expect("pending local transactions must not query the datasource"); + + assert_eq!(response.value.len(), 1); + assert!(response.value[0].is_none()); + setup + .context + .svm_locker + .mark_transaction_complete(&missing_signature); + } + + let result = setup + .rpc + .get_signature_statuses( + Some(setup.context), + vec![missing_signature.to_string()], + Some(RpcSignatureStatusConfig { + search_transaction_history: true, + }), + ) + .await + .expect("history lookup should accept a missing upstream transaction"); + + assert_eq!(result.value.len(), 1); + assert!(result.value[0].is_none()); + datasource_request + .await + .expect("test datasource should receive the history lookup"); + } + + #[test] + fn test_send_transaction_clears_pending_signature_when_enqueue_fails() { + let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded(); + drop(mempool_rx); + let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx); + let payer = Keypair::new(); + let recent_blockhash = setup + .context + .svm_locker + .with_svm_reader(|svm_reader| svm_reader.latest_blockhash()); + let transaction = + build_legacy_transaction(&payer.pubkey(), &[&payer], &[], &recent_blockhash); + let signature = transaction.signatures[0]; + + let result = setup.rpc.send_transaction( + Some(setup.context.clone()), + bs58::encode(bincode::serialize(&transaction).unwrap()).into_string(), + None, + ); + + assert!(result.is_err()); + assert!(!setup.context.svm_locker.is_transaction_pending(&signature)); + } + #[test] fn test_request_airdrop() { let pk = Pubkey::new_unique(); diff --git a/crates/core/src/runloops/mod.rs b/crates/core/src/runloops/mod.rs index 8a59e56ad..46d3eda61 100644 --- a/crates/core/src/runloops/mod.rs +++ b/crates/core/src/runloops/mod.rs @@ -505,7 +505,10 @@ pub async fn start_block_production_runloop( SimnetCommand::ProcessTransaction(_key, transaction, status_tx, skip_preflight, skip_sig_verify_override) => { let skip_sig_verify = skip_sig_verify_override.unwrap_or(global_skip_sig_verify); let sigverify = !skip_sig_verify; - if let Err(e) = svm_locker.process_transaction(&remote_client_with_commitment, transaction, status_tx, skip_preflight, sigverify).await { + let signature = transaction.signatures[0]; + let result = svm_locker.process_transaction(&remote_client_with_commitment, transaction, status_tx, skip_preflight, sigverify).await; + svm_locker.mark_transaction_complete(&signature); + if let Err(e) = result { svm_locker.simnet_events_tx().error(format!("Failed to process transaction: {}", e)); } if block_production_mode.eq(&BlockProductionMode::Transaction) { diff --git a/crates/core/src/surfnet/locker.rs b/crates/core/src/surfnet/locker.rs index c6e4de48a..443501b5c 100644 --- a/crates/core/src/surfnet/locker.rs +++ b/crates/core/src/surfnet/locker.rs @@ -94,6 +94,12 @@ enum ProcessTransactionResult { ExecutionFailure(FailedTransactionMetadata), } +struct LocalTransactionLookup { + result: GetTransactionResult, + is_pending: bool, + latest_absolute_slot: Slot, +} + pub struct SvmAccessContext { pub slot: Slot, pub latest_epoch_info: EpochInfo, @@ -1606,6 +1612,19 @@ impl SurfnetSvmLocker { /// Functions for getting transactions from the underlying SurfnetSvm instance or remote client impl SurfnetSvmLocker { + pub(crate) fn mark_transaction_pending(&self, signature: Signature) { + self.with_svm_writer(|svm_writer| svm_writer.mark_transaction_pending(signature)); + } + + pub(crate) fn mark_transaction_complete(&self, signature: &Signature) { + self.with_svm_writer(|svm_writer| svm_writer.mark_transaction_complete(signature)); + } + + #[cfg(test)] + pub(crate) fn is_transaction_pending(&self, signature: &Signature) -> bool { + self.with_svm_reader(|svm_reader| svm_reader.is_transaction_pending(signature)) + } + /// Retrieves a transaction by signature, using local or remote based on context. pub async fn get_transaction( &self, @@ -1647,11 +1666,26 @@ impl SurfnetSvmLocker { signature: &Signature, config: &RpcTransactionConfig, ) -> SurfpoolResult { + Ok(self + .get_transaction_local_with_pending(signature, config)? + .result) + } + + fn get_transaction_local_with_pending( + &self, + signature: &Signature, + config: &RpcTransactionConfig, + ) -> SurfpoolResult { self.with_svm_reader(|svm_reader| { let latest_absolute_slot = svm_reader.get_latest_absolute_slot(); + let is_pending = svm_reader.is_transaction_pending(signature); let Some(entry) = svm_reader.transactions.get(&signature.to_string())? else { - return Ok(GetTransactionResult::None(*signature)); + return Ok(LocalTransactionLookup { + result: GetTransactionResult::None(*signature), + is_pending, + latest_absolute_slot, + }); }; let (transaction_with_status_meta, _) = entry.expect_processed(); @@ -1666,16 +1700,20 @@ impl SurfnetSvmLocker { config.max_supported_transaction_version, true, )?; - Ok(GetTransactionResult::found_transaction( - *signature, - EncodedConfirmedTransactionWithStatusMeta { - slot, - transaction: encoded, - block_time, - transaction_index: None, - }, + Ok(LocalTransactionLookup { + result: GetTransactionResult::found_transaction( + *signature, + EncodedConfirmedTransactionWithStatusMeta { + slot, + transaction: encoded, + block_time, + transaction_index: None, + }, + latest_absolute_slot, + ), + is_pending, latest_absolute_slot, - )) + }) }) } @@ -1686,14 +1724,14 @@ impl SurfnetSvmLocker { signature: &Signature, config: RpcTransactionConfig, ) -> SurfpoolResult { - let local_result = self.get_transaction_local(signature, &config)?; - let latest_absolute_slot = self.get_latest_absolute_slot(); - if local_result.is_none() { + let local_lookup = self.get_transaction_local_with_pending(signature, &config)?; + + if local_lookup.result.is_none() && !local_lookup.is_pending { client - .try_get_transaction(*signature, config, latest_absolute_slot) + .try_get_transaction(*signature, config, local_lookup.latest_absolute_slot) .await } else { - Ok(local_result) + Ok(local_lookup.result) } } } diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 6508463f5..0e65972cf 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -311,6 +311,12 @@ pub struct SurfnetSvm { pub chain_tip: BlockIdentifier, pub blocks: Box>, pub transactions: Box>, + /// Signatures accepted by `sendTransaction` that have not finished processing yet. + /// + /// Counts are used because clients may submit the same signed transaction concurrently. + /// Keeping this next to `transactions` lets readers atomically distinguish a genuinely + /// unknown signature from one that is waiting in the runloop queue. + pending_transaction_signatures: HashMap, pub jito_bundles: Box>>, pub transactions_queued_for_confirmation: VecDeque<( VersionedTransaction, @@ -523,6 +529,28 @@ fn synthetic_blockhash_for_slot(slot: Slot, genesis_slot: Slot) -> SyntheticBloc } impl SurfnetSvm { + pub(crate) fn mark_transaction_pending(&mut self, signature: Signature) { + *self + .pending_transaction_signatures + .entry(signature) + .or_default() += 1; + } + + pub(crate) fn mark_transaction_complete(&mut self, signature: &Signature) { + let Some(pending_count) = self.pending_transaction_signatures.get_mut(signature) else { + return; + }; + + *pending_count -= 1; + if *pending_count == 0 { + self.pending_transaction_signatures.remove(signature); + } + } + + pub(crate) fn is_transaction_pending(&self, signature: &Signature) -> bool { + self.pending_transaction_signatures.contains_key(signature) + } + pub fn default() -> (Self, Receiver, Receiver) { Self::new(SurfnetSvmConfig::default()).unwrap() } @@ -574,6 +602,8 @@ impl SurfnetSvm { // Wrap all storage fields with OverlayStorage blocks: OverlayStorage::wrap(self.blocks.clone_box()), transactions: OverlayStorage::wrap(self.transactions.clone_box()), + // Profiling sandboxes do not consume the live transaction command queue. + pending_transaction_signatures: HashMap::new(), jito_bundles: OverlayStorage::wrap(self.jito_bundles.clone_box()), profile_tag_map: OverlayStorage::wrap(self.profile_tag_map.clone_box()), simulated_transaction_profiles: OverlayStorage::wrap( @@ -1053,6 +1083,7 @@ impl SurfnetSvm { chain_tip, blocks: blocks_db, transactions: transactions_db, + pending_transaction_signatures: HashMap::new(), jito_bundles: jito_bundles_db, perf_samples: VecDeque::new(), transactions_processed, diff --git a/crates/core/src/tests/integration.rs b/crates/core/src/tests/integration.rs index cd4e51fbf..b3331813a 100644 --- a/crates/core/src/tests/integration.rs +++ b/crates/core/src/tests/integration.rs @@ -11449,7 +11449,7 @@ async fn test_send_transaction_skip_sig_verify_processes_and_updates_state(test_ let svm_locker = SurfnetSvmLocker::new(surfnet_svm); let _runloop = spawn_runloop( - svm_locker, + svm_locker.clone(), config, (simnet_commands_tx, simnet_commands_rx), geyser_events_rx, @@ -11522,6 +11522,7 @@ async fn test_send_transaction_skip_sig_verify_processes_and_updates_state(test_ }, skip_sig_verify: Some(true), }; + let signature = tx.signatures[0]; let _sig = full_client .send_transaction(data, Some(send_config)) .await @@ -11537,6 +11538,13 @@ async fn test_send_transaction_skip_sig_verify_processes_and_updates_state(test_ } }) .await; + tokio::time::timeout(Duration::from_secs(1), async { + while svm_locker.0.read().await.is_transaction_pending(&signature) { + tokio::task::yield_now().await; + } + }) + .await + .expect("the runloop should clear the pending signature after processing"); // Assert recipient received the transfer let final_recipient_balance = minimal_client From 66472cd6b0b86c993d6ae4f176d747f2997f76b6 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Wed, 2 Sep 2026 13:01:26 -0400 Subject: [PATCH 06/13] feat(core): update litesvm to enable full txv1 features (#780) --- Cargo.lock | 805 +++++++++++------- Cargo.toml | 53 +- crates/core/Cargo.toml | 3 + crates/core/src/rpc/full.rs | 79 +- crates/core/src/rpc/surfnet_cheatcodes.rs | 126 +-- crates/core/src/rpc/utils.rs | 90 +- crates/core/src/tests/integration.rs | 521 +++++++++++- crates/core/src/types.rs | 167 ++-- crates/sdk-node/surfpool-sdk/kit/types/api.ts | 2 +- 9 files changed, 1285 insertions(+), 561 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2809381f1..141bce739 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -282,9 +282,9 @@ dependencies = [ [[package]] name = "agave-bls-cert-verify" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fe72b5fc19a84cc69bb22c5c16927b25020fea0bcdb8e34d3bdb4b4a65f4b8" +checksum = "f6b6334f25735d4345cc57ae8a9f7da3d99f422dfd16defe8bf253f4716a0f45" dependencies = [ "agave-votor-messages", "bitvec", @@ -292,15 +292,24 @@ dependencies = [ "rayon", "solana-bls-signatures", "solana-signer-store", - "thiserror 2.0.18", + "thiserror 2.0.20", "wincode", ] +[[package]] +name = "agave-cpu-utils" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2da49b780e1831abaef6fd17016b653cf21301ad77271b72ad477c530029b49" +dependencies = [ + "libc", +] + [[package]] name = "agave-feature-set" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4790979fc2a3c1c494c9cb84e8b514da4b8c6a992a460a077b31b07657606f8" +checksum = "93723b88193a51692304c79dc52cfea0389dabe9a6650288dd508b78fbff5a8f" dependencies = [ "ahash", "solana-epoch-schedule 3.2.0", @@ -313,9 +322,9 @@ dependencies = [ [[package]] name = "agave-fs" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6ffe1eff4e50caa9ef14e7b706ce7d775fc31f53fe95f4c140f3f776f2e913" +checksum = "36bfb38931e2aafc6e72183db7b9ecb40f5bf8ea1783444905eab51f0830e2e7" dependencies = [ "agave-io-uring", "io-uring", @@ -324,14 +333,14 @@ dependencies = [ "slab", "smallvec", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "agave-geyser-plugin-interface" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cfcdaf74b41fc2a8446a9659de85fae8a726b53a6c6396f86d9d21599059be" +checksum = "b31e6c467174ad4e846946791fa2b1bdf3c1bca915b7fdfbac02828ec97a58a2" dependencies = [ "log 0.4.33", "solana-clock 3.1.1", @@ -340,14 +349,14 @@ dependencies = [ "solana-signature", "solana-transaction 4.1.5", "solana-transaction-status", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "agave-io-uring" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaeab143a6f0fa8387b07b8fb23d75aa3873f1ae9a84dfb4ada11297cffecf10" +checksum = "ff3fc82af9dde3830ba232d5b9696501efe718f548b5ca9276c0a692a265b180" dependencies = [ "io-uring", "libc", @@ -358,9 +367,9 @@ dependencies = [ [[package]] name = "agave-precompiles" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c451389a4e48da05d1eddc6d76bbc88f07cf4fea08c806dcd73eeddee199313a" +checksum = "3d4b7f6df4556094a86b0cfedc931235ddbb738f3a8a0676990f0be26091cca9" dependencies = [ "agave-feature-set", "bincode", @@ -379,18 +388,18 @@ dependencies = [ [[package]] name = "agave-random" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2298425bb1b2b72514242c80b5086521ad0b09a9f61dfbc1df033e9a295e50e3" +checksum = "88a9ee7bf8f8fc56921efe41d4ca910bdbe8846aaad7ac0a9c3d74b2cc68b145" dependencies = [ "rand 0.9.4", ] [[package]] name = "agave-reserved-account-keys" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea33710f30b13f62e6d73dbbb114117029d29539cb9439c30ef39a0736f935cc" +checksum = "823f261d896cedb7f95ecfed70d27aae6c7fd5a8fcc01e21a71382268e0743e4" dependencies = [ "agave-feature-set", "solana-pubkey 4.2.0", @@ -399,9 +408,9 @@ dependencies = [ [[package]] name = "agave-snapshots" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38a4e8b130fedd43e6acf9cf702162339fac2e0f821d4bd9bef757ba2285679" +checksum = "827c148711b74d41d75772b9171ce62a3b05f36b74e8d9c6d48b38b9b21bb770" dependencies = [ "agave-fs", "bincode", @@ -423,37 +432,36 @@ dependencies = [ "symlink", "tar", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "wincode", "zstd", ] [[package]] name = "agave-transaction-view" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f809b8332d13fbe3f4f1529a9806ff21c966447d2415bb55881983f0f35459d" +checksum = "355713e066b40689fca695caa2cb4f3fa582dc7a40c3fa28206b84ad5fa6bf95" dependencies = [ "solana-hash 4.5.0", "solana-message 4.3.0", "solana-packet", - "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids 3.1.0", "solana-short-vec 3.2.2", "solana-signature", "solana-svm-transaction", "solana-transaction 4.1.5", - "solana-transaction-context", ] [[package]] name = "agave-votor-messages" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843026e90ca80b870bb08be92a29093e50a13dc88e38b744cb0c8580ba06089b" +checksum = "f13132a1b3bb9c543f19b6de6d47318c6ed6af7978808d9abf1f424c31da2f01" dependencies = [ "agave-feature-set", + "crossbeam-channel", "log 0.4.33", "serde", "solana-address 2.6.1", @@ -464,33 +472,35 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-short-vec 3.2.2", "solana-signer-store", - "thiserror 2.0.18", + "thiserror 2.0.20", "wincode", ] [[package]] name = "agave-xdp" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8e01336dd8ebac19f2a0cdea7546c2605dc0dc3fc09ee92ec7e7497366303a" +checksum = "1184194019693cf5e430205410847f2b18234270052ece5ed1ac0200cb4ba9f8" dependencies = [ + "agave-cpu-utils", "agave-xdp-ebpf", "arc-swap", + "arrayvec", "aya", "bytes 1.12.1", "caps", - "core_affinity", "crossbeam-channel", + "crossbeam-queue", "libc", "log 0.4.33", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "agave-xdp-ebpf" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccf0f26e84ab010364eccc570627f84c0d06d78f7677d96062eaef10801b648" +checksum = "089340aa6476eea624a152f75416dce28b5e0ee425e7d3c5bfa4a96d43a606d7" dependencies = [ "aya", "aya-ebpf", @@ -943,7 +953,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", ] @@ -1121,19 +1131,20 @@ dependencies = [ [[package]] name = "aya" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" +checksum = "66e644424fada9fff4fdc63848db1732fb69b626e8328202ef55c03df1f4d939" dependencies = [ "assert_matches", "aya-obj", "bitflags 2.13.0", - "bytes 1.12.1", + "hashbrown 0.17.1", "libc", "log 0.4.33", "object", "once_cell", - "thiserror 1.0.69", + "scopeguard", + "thiserror 2.0.20", ] [[package]] @@ -1191,16 +1202,14 @@ dependencies = [ [[package]] name = "aya-obj" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51b96c5a8ed8705b40d655273bc4212cbbf38d4e3be2788f36306f154523ec7" +checksum = "8c76b9c75d9cdc155ff8f6a06d61e873f67bf47be8cfa92a3b5aaea43f4b4077" dependencies = [ "bytes 1.12.1", - "core-error", - "hashbrown 0.15.5", "log 0.4.33", "object", - "thiserror 1.0.69", + "thiserror 2.0.20", ] [[package]] @@ -1685,7 +1694,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -2063,15 +2072,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "core-error" -version = "0.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efcdb2972eb64230b4c50646d8498ff73f5128d196a90c7236eec4cbe8619b8f" -dependencies = [ - "version_check", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -2098,17 +2098,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core_affinity" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" -dependencies = [ - "libc", - "num_cpus", - "winapi 0.3.9", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2544,7 +2533,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -2814,29 +2803,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi 0.3.9", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "dotenvy" version = "0.15.7" @@ -3666,6 +3632,10 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hcl-edit" @@ -3764,6 +3734,15 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.8.1" @@ -4379,6 +4358,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -4456,7 +4444,7 @@ dependencies = [ "jni-sys", "log 0.4.33", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -5032,9 +5020,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litesvm" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f63c450e355f92115e6ef13177bd53e22a0ffc16638d81ae282c1379c5364a" +checksum = "a286f10b45b3a8efd78f47846d34af8d109ff5684c2b10a11920b5b089ad6e79" dependencies = [ "agave-feature-set", "agave-precompiles", @@ -5042,7 +5030,7 @@ dependencies = [ "ansi_term", "hex", "indexmap 2.14.0", - "itertools 0.14.0", + "itertools 0.15.0", "log 0.4.33", "nom 8.0.0", "qualifier_attr", @@ -5059,6 +5047,7 @@ dependencies = [ "solana-epoch-schedule 3.2.0", "solana-feature-gate-interface 4.0.0", "solana-fee", + "solana-fee-calculator 3.2.2", "solana-fee-structure", "solana-hash 4.5.0", "solana-instruction 3.4.0", @@ -5076,8 +5065,10 @@ dependencies = [ "solana-program-error 3.0.1", "solana-program-runtime", "solana-rent 4.3.0", + "solana-runtime-transaction", "solana-sdk-ids 3.1.0", "solana-sha256-hasher 3.1.0", + "solana-short-vec 3.2.2", "solana-signature", "solana-signer", "solana-slot-hashes 3.1.0", @@ -5095,15 +5086,15 @@ dependencies = [ "solana-transaction 4.1.5", "solana-transaction-context", "solana-transaction-error 3.3.1", - "thiserror 2.0.18", + "thiserror 2.0.20", "wincode", ] [[package]] name = "litesvm-token" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86cce45acda3911a98a578f1d1af19b8a357cb255f25230469b66439fbc011f1" +checksum = "3cc0d806375b3854f5fa0e27976ce536e9c112486bb3f46a4cda29d55f6762c8" dependencies = [ "litesvm", "smallvec", @@ -5656,6 +5647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -5707,12 +5699,12 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "object" -version = "0.36.7" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "indexmap 2.14.0", "memchr", ] @@ -5819,7 +5811,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror 2.0.20", "tracing", ] @@ -5833,7 +5825,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror 2.0.20", "tracing", ] @@ -5862,7 +5854,7 @@ dependencies = [ "futures-util", "glob", "opentelemetry 0.28.0", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", ] @@ -5879,7 +5871,7 @@ dependencies = [ "opentelemetry 0.31.0", "percent-encoding 2.3.2", "rand 0.9.4", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -6435,7 +6427,7 @@ dependencies = [ "rustc-hash 2.1.3", "rustls 0.23.41", "socket2 0.6.4", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -6459,7 +6451,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror 2.0.18", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -6739,7 +6731,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -6954,7 +6946,7 @@ dependencies = [ "serde", "serde_json", "sse-stream", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", "tokio-util 0.7.18", @@ -7006,7 +6998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -7400,9 +7392,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -7429,22 +7421,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.4", ] [[package]] @@ -7913,9 +7905,9 @@ dependencies = [ [[package]] name = "solana-account-decoder" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1daf7a9ad1ba8952f79d02ff2ff2f75d789bfb9c33aa53da4a377de86c5da02e" +checksum = "5d539d57ab52c20309711d54fc309adcf7ed1c688277c8ac6437db34c5be39f7" dependencies = [ "Inflector", "base64 0.22.1", @@ -7941,23 +7933,25 @@ dependencies = [ "solana-sdk-ids 3.1.0", "solana-slot-hashes 3.1.0", "solana-slot-history 3.1.0", - "solana-stake-interface 3.1.0", + "solana-stake-interface 4.3.0", "solana-sysvar 4.1.0", "solana-vote-interface 6.0.3", + "solana-zk-sdk-pod", "spl-generic-token", - "spl-token-2022-interface", + "spl-token-2022-interface 3.1.1", "spl-token-group-interface", - "spl-token-interface 2.0.0", - "spl-token-metadata-interface", - "thiserror 2.0.18", + "spl-token-interface 3.0.0", + "spl-token-metadata-interface 1.0.1", + "thiserror 2.0.20", + "wincode", "zstd", ] [[package]] name = "solana-account-decoder-client-types" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c4eb99bb799650f2c6cb7291d93cf6db05e956320f3c772e58ca49f8b6192e" +checksum = "ce9eca88c8336e46a990fd6541eadee77685ecb84d771ade0d2ed4cc5801f438" dependencies = [ "base64 0.22.1", "bs58", @@ -7994,9 +7988,9 @@ dependencies = [ [[package]] name = "solana-accounts-db" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d48a39c4c0cfc09e1cb70635f467707d63b80b259eb4a96ec0fc8e9f7145be2" +checksum = "07531b1145a518434fd090d6eda600ec581490a9c6eff81083276ec829d8c946" dependencies = [ "agave-fs", "ahash", @@ -8005,7 +7999,6 @@ dependencies = [ "dashmap", "itertools 0.14.0", "log 0.4.33", - "memmap2 0.9.11", "modular-bitfield", "num_cpus", "rand 0.9.4", @@ -8038,7 +8031,7 @@ dependencies = [ "solana-transaction-error 3.3.1", "spl-generic-token", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -8139,17 +8132,6 @@ dependencies = [ "solana-define-syscall 2.3.0", ] -[[package]] -name = "solana-big-mod-exp" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c80fb6d791b3925d5ec4bf23a7c169ef5090c013059ec3ed7d0b2c04efa085" -dependencies = [ - "num-bigint 0.4.8", - "num-traits", - "solana-define-syscall 3.0.0", -] - [[package]] name = "solana-bincode" version = "2.2.1" @@ -8217,7 +8199,7 @@ dependencies = [ "solana-signature", "solana-signer", "subtle", - "thiserror 2.0.18", + "thiserror 2.0.20", "wincode", "zeroize", ] @@ -8248,7 +8230,7 @@ dependencies = [ "ark-serialize 0.5.0", "bytemuck", "solana-define-syscall 5.1.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -8272,9 +8254,9 @@ dependencies = [ [[package]] name = "solana-bpf-loader-program" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "073e0a6555480e8f783f930299a5660c2296c8961d9b3a2adaf585561e2d59b9" +checksum = "fba6be8c60af6076de7e40e54a0ad9d88ad5f7004ecf9be5e93d296dd9b0cbe3" dependencies = [ "bincode", "qualifier_attr", @@ -8299,9 +8281,9 @@ dependencies = [ [[package]] name = "solana-bucket-map" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ec27feb51bf40050caf4935c0cb8e67af9fde4246775e89cc37ceacb3d56982" +checksum = "1454088c126ab007f2dc0ffc01da1371d72b02ab1cef882aaefbfc23b08b54f3" dependencies = [ "ahash", "bv", @@ -8319,9 +8301,9 @@ dependencies = [ [[package]] name = "solana-builtins" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e54d03edadcb2f38bdb8b6524380a858f5ad51654db1f71eccc3fb5dc4d492" +checksum = "df6d910a669a1093fef471ffec05e5c69e632a83788d13f7703ece5cba62db5b" dependencies = [ "agave-feature-set", "solana-bpf-loader-program", @@ -8338,9 +8320,9 @@ dependencies = [ [[package]] name = "solana-builtins-default-costs" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a63b9b3ae7b09c32f7f15d83e875dbf8715d0853c2354e3306316158f19d9c90" +checksum = "78a9b9bac7336baf78fcce234a76419940f2b2c0dc10f88aa0a7866a6eebffa7" dependencies = [ "agave-feature-set", "ahash", @@ -8350,9 +8332,9 @@ dependencies = [ [[package]] name = "solana-client" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15343115e7efd0a7420f6e164462c5fddad99fb1bf0acd0ed26ff3a78cc72977" +checksum = "aa7cfda2a159cc5f5ff6bd969df7b3bcf147e8ca18f20de5edb2d3e72e65a585" dependencies = [ "async-trait", "bincode", @@ -8457,9 +8439,9 @@ dependencies = [ [[package]] name = "solana-compute-budget" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1770e5aeba2f5ec75b2d89031780c760dcf8352a061430f5e74eab1ad9f3b132" +checksum = "508ef8407ce095d146830943bab8dccbbc6d581615e1de8d449565d16eae3aff" dependencies = [ "solana-fee-structure", "solana-program-runtime", @@ -8467,9 +8449,9 @@ dependencies = [ [[package]] name = "solana-compute-budget-instruction" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3ad15d715b0c4e21f6b591f126b7fa8070aadf0d75fcd89bc4c98afc9e5f8f6" +checksum = "8d9fbc30be58ea9c93c153d48b6e3b7fb646d7c7492484837d43790c952decef" dependencies = [ "agave-feature-set", "solana-borsh 3.0.2", @@ -8497,9 +8479,9 @@ dependencies = [ [[package]] name = "solana-compute-budget-program" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653943375fb64b11bcfda0c22280aa87f04902d2c25d0117e91f9788a31ed3db" +checksum = "c7662ea666daafd5028d981671197d61bc9151e96326749a029bf69ae4cab139" dependencies = [ "solana-program-runtime", ] @@ -8523,9 +8505,9 @@ dependencies = [ [[package]] name = "solana-connection-cache" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ac04535ebeed354141f8f0f12faaa22e75017e7c789835cfddbef9432eeb29" +checksum = "ca6f4ed1ce889b629cb77c18066bf9612407a7aeac8f99bda3ca4986fdac5d12" dependencies = [ "async-trait", "crossbeam-channel", @@ -8537,14 +8519,14 @@ dependencies = [ "solana-metrics", "solana-time-utils", "solana-transaction-error 3.3.1", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "solana-cost-model" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2b715975b4148e3197d0de23d0e4777bd39037b40cceb2fe437c8e9b320a41" +checksum = "a51370a4107336b4f180b0597f1320199f7faca9db5334317662694478456ca2" dependencies = [ "agave-feature-set", "ahash", @@ -8604,7 +8586,7 @@ dependencies = [ "curve25519-dalek 4.1.3", "solana-define-syscall 3.0.0", "subtle", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -8618,7 +8600,7 @@ dependencies = [ "curve25519-dalek 4.1.3", "solana-define-syscall 5.1.0", "subtle", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -8679,13 +8661,12 @@ dependencies = [ [[package]] name = "solana-entry" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c0bba8fcd48dacc450f8d0202d0cc17f4cea6a9fc621cbcb4b7bb7733c1582" +checksum = "1aa192f613474b8e2ba779db5098f729c8d6ce092f96b6d67ff9b3ed15bb821e" dependencies = [ "agave-votor-messages", "crossbeam-channel", - "dlopen2", "log 0.4.33", "num_cpus", "rayon", @@ -8693,6 +8674,7 @@ dependencies = [ "solana-address 2.6.1", "solana-bls-signatures", "solana-clock 3.1.1", + "solana-cost-model", "solana-hash 4.5.0", "solana-measure", "solana-merkle-tree", @@ -8704,7 +8686,7 @@ dependencies = [ "solana-signature", "solana-transaction 4.1.5", "solana-transaction-error 3.3.1", - "thiserror 2.0.18", + "thiserror 2.0.20", "wincode", ] @@ -8806,7 +8788,7 @@ dependencies = [ "solana-pubkey 2.4.0", "solana-sdk-ids 2.2.1", "solana-system-interface 1.0.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -8860,9 +8842,9 @@ dependencies = [ [[package]] name = "solana-fee" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbffe004ec900267fc0bcf0c3b532c01905e6c4e3f318562718d1cdf6db939" +checksum = "f6b05fdbd78318f6832af2a49d14b59799f4783ff3a307c4a25b50896b57b352" dependencies = [ "agave-feature-set", "solana-fee-structure", @@ -9202,9 +9184,9 @@ dependencies = [ [[package]] name = "solana-lattice-hash" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4dc0c380275da3d140f6027966ee35e0d647af735042c5cf8d4f58bb6fa99ac" +checksum = "ebd93ae529a0bc28774d993b8c0da7364fcb69e2c1bc27d74fb1d810ee9de907" dependencies = [ "base64 0.22.1", "blake3", @@ -9214,9 +9196,9 @@ dependencies = [ [[package]] name = "solana-leader-schedule" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5729553d41d01fbf96a416fc7ca507b19c7b662781055c98412db3c27c303d66" +checksum = "8f8348c203b2b35fa6bbddf98deeab53732a92ddf32fe0616e1a0d73470fa74f" dependencies = [ "agave-random", "itertools 0.14.0", @@ -9340,15 +9322,15 @@ dependencies = [ [[package]] name = "solana-measure" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ac715928e7c5aaded3308701e6e5412c621ed69167404df9ece96b86c8295b3" +checksum = "7efcc69fcf820688a4ef183e993eb9de5bf3d56ff64aeea8b8afdc121f81afe3" [[package]] name = "solana-merkle-tree" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ebd6884c8d8855dba4494a56875a5ef86bdff21016c5f4af272e471b303545" +checksum = "21c112b73bd703a41e0cbac97377c2d4775199465a738c01cfe33afaaac87745" dependencies = [ "fast-math", "solana-hash 4.5.0", @@ -9419,9 +9401,9 @@ dependencies = [ [[package]] name = "solana-metrics" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898e217d6b99e2b5f2f2dc11f5d8df523dd63f6dad8fc18e068610fc1b6e241e" +checksum = "3b89da7b1dd661c8a94772e6d6af8af812dd5aabafb93ce99acfdb1e2482d96c" dependencies = [ "crossbeam-channel", "gethostname", @@ -9430,7 +9412,7 @@ dependencies = [ "solana-cluster-type", "solana-sha256-hasher 3.1.0", "solana-time-utils", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -9465,13 +9447,15 @@ checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" [[package]] name = "solana-net-utils" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c8d565693d6551f0ff344edf257b5a92723b6b5b938902413bb8b7cd61e1" +checksum = "1c04c9afe14ec096909cf7ea8d2357adb44379519c2c9b017312314e66f17931" dependencies = [ + "agave-xdp", "bincode", "bytes 1.12.1", "cfg-if 1.0.4", + "crossbeam-channel", "dashmap", "itertools 0.14.0", "log 0.4.33", @@ -9481,7 +9465,7 @@ dependencies = [ "socket2 0.6.4", "solana-serde", "solana-svm-type-overrides", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "url 2.5.8", ] @@ -9540,6 +9524,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7b49f68ccea949a6ea75f485dbe2e17cf4c1d894ed262371d584269359e1a0" dependencies = [ + "borsh 1.7.0", "bytemuck", ] @@ -9560,12 +9545,13 @@ dependencies = [ [[package]] name = "solana-perf" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17eff8912e1ba0218f506be0e0fead572bbd7eaaa7f841ab2d16ccc94356521d" +checksum = "8a83ec3296547440ea4257333a82e9ac74327b56772c210da7be8b38c395c53d" dependencies = [ "agave-transaction-view", "ahash", + "arc-swap", "bincode", "bytes 1.12.1", "caps", @@ -9581,11 +9567,13 @@ dependencies = [ "solana-metrics", "solana-packet", "solana-pubkey 4.2.0", + "solana-runtime-transaction", "solana-sdk-ids 3.1.0", "solana-short-vec 3.2.2", "solana-signature", "solana-time-utils", "solana-transaction-context", + "wincode", ] [[package]] @@ -9609,7 +9597,7 @@ dependencies = [ "light-poseidon 0.2.0", "light-poseidon 0.4.0", "solana-define-syscall 4.0.1", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -9649,7 +9637,7 @@ dependencies = [ "solana-account-info 2.3.0", "solana-address-lookup-table-interface 2.2.2", "solana-atomic-u64 2.2.1", - "solana-big-mod-exp 2.2.1", + "solana-big-mod-exp", "solana-bincode 2.2.1", "solana-blake3-hasher 2.2.1", "solana-borsh 2.2.1", @@ -9697,7 +9685,7 @@ dependencies = [ "solana-sysvar 2.3.0", "solana-sysvar-id 2.2.1", "solana-vote-interface 2.2.6", - "thiserror 2.0.18", + "thiserror 2.0.20", "wasm-bindgen", ] @@ -9800,9 +9788,9 @@ dependencies = [ [[package]] name = "solana-program-runtime" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0364032ca71f9be2ffab27791cd6535dbe9d2990da0e2346e99013a9bcb57f4" +checksum = "99a3895314c34396758131a8af156944453ebe18e70d2a5cde67eec899df48a1" dependencies = [ "base64 0.22.1", "bincode", @@ -9829,7 +9817,7 @@ dependencies = [ "solana-sdk-ids 3.1.0", "solana-slot-hashes 3.1.0", "solana-stable-layout 3.0.1", - "solana-stake-interface 3.1.0", + "solana-stake-interface 4.3.0", "solana-svm-callback", "solana-svm-feature-set", "solana-svm-log-collector", @@ -9841,7 +9829,8 @@ dependencies = [ "solana-sysvar 4.1.0", "solana-sysvar-id 3.1.0", "solana-transaction-context", - "thiserror 2.0.18", + "thiserror 2.0.20", + "wincode", ] [[package]] @@ -9891,9 +9880,9 @@ dependencies = [ [[package]] name = "solana-pubsub-client" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bf82e3bc5265cbac1676fadfe64d9b0f7b889d9b75a6ec78ccacd052f3d7c1" +checksum = "970f5e82e1ad2988b4c87184df74da46d72ee367ee906c4be2cadda575b395a3" dependencies = [ "crossbeam-channel", "futures-util", @@ -9905,7 +9894,7 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-rpc-client-types", "solana-signature", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", "tokio-tungstenite 0.28.0", @@ -9915,9 +9904,9 @@ dependencies = [ [[package]] name = "solana-quic-client" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f94e5546a008014b89b20eef8524069b8b6bf23333210e764edb4162360ce87" +checksum = "cf8a8b63bd7344c2ea92ba41f05bcb6f1923d5a6299c450a1db1913a36fac3b1" dependencies = [ "async-lock", "async-trait", @@ -9935,15 +9924,15 @@ dependencies = [ "solana-streamer", "solana-tls-utils", "solana-transaction-error 3.3.1", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", ] [[package]] name = "solana-rayon-threadlimit" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7c1cd44a82c0576483256c43f0205277c176531ea18e815e3d4a247a9a70b6" +checksum = "3bb2a5053b25dccee924964f1b9a32b755cb6cb4bcff64d8b8d3bcea53bf78ac" dependencies = [ "log 0.4.33", "num_cpus", @@ -10001,10 +9990,11 @@ dependencies = [ [[package]] name = "solana-rpc-client" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "849a4576002de8ba30ebb60aa4067b82643b23907be874786473736c616c24ec" +checksum = "b8b6e398f6df7a0d260d5adb1bc705b267432647f6d63c1f9084cf43cf60330c" dependencies = [ + "agave-votor-messages", "async-trait", "base64 0.22.1", "bincode", @@ -10020,6 +10010,7 @@ dependencies = [ "solana-account 4.3.1", "solana-account-decoder", "solana-account-decoder-client-types", + "solana-bls-signatures", "solana-clock 3.1.1", "solana-commitment-config", "solana-epoch-info", @@ -10037,13 +10028,14 @@ dependencies = [ "solana-version", "solana-vote-interface 6.0.3", "tokio", + "wincode", ] [[package]] name = "solana-rpc-client-api" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147f376d3770f495f0bd9158467198cbb399c9a7395ea35ed587061a1a59e61d" +checksum = "c6f007fc3629ffcda78189a82fee168322edc1c31cb0e792f468455c7bbe3153" dependencies = [ "anyhow", "jsonrpc-core", @@ -10056,14 +10048,14 @@ dependencies = [ "solana-signer", "solana-transaction-error 3.3.1", "solana-transaction-status-client-types", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "solana-rpc-client-nonce-utils" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c2e921c1ed9e8ba2aa6cba40d8bbdbe13cb4520006edbc2d9915f4713dc4cf" +checksum = "b47e4b351c333cb3c0fd771c09e00901e27572468022d322fb1b8e5532c3deba" dependencies = [ "solana-account 4.3.1", "solana-commitment-config", @@ -10073,14 +10065,14 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-rpc-client", "solana-sdk-ids 3.1.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "solana-rpc-client-types" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae18406ee879a0d91da081313f591d1b3e1c370e010c63ee1b1a3741db88693e" +checksum = "971b17e4f9371aecbf01846b0102f1cb1479da1d1bd98b67f9fd1b12e6ac591c" dependencies = [ "base64 0.22.1", "bs58", @@ -10098,14 +10090,14 @@ dependencies = [ "solana-transaction-error 3.3.1", "solana-transaction-status-client-types", "solana-version", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "solana-runtime" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186f17af8a38b26597b23ab48f5afadecc87a0931a560119ed2bea978e4d1c3b" +checksum = "71c5394546bf8e1a6408dc8b3562ea2cd03c402e8c96cc7dd492274d8b9f047a" dependencies = [ "agave-bls-cert-verify", "agave-feature-set", @@ -10121,8 +10113,10 @@ dependencies = [ "assert_matches", "base64 0.22.1", "bincode", + "bitvec", "bytemuck", "crossbeam-channel", + "crossbeam-utils", "dashmap", "imbl", "itertools 0.14.0", @@ -10131,7 +10125,6 @@ dependencies = [ "mockall", "num-derive", "num-traits", - "num_cpus", "percentage", "qualifier_attr", "rand 0.9.4", @@ -10202,7 +10195,7 @@ dependencies = [ "solana-signer-store", "solana-slot-hashes 3.1.0", "solana-slot-history 3.1.0", - "solana-stake-interface 3.1.0", + "solana-stake-interface 4.3.0", "solana-svm", "solana-svm-callback", "solana-svm-timings", @@ -10226,17 +10219,16 @@ dependencies = [ "static_assertions", "strum 0.28.0", "strum_macros 0.28.0", - "symlink", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "wincode", ] [[package]] name = "solana-runtime-transaction" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c380a9609b42a286fe592b0309975df75f10dd239edcd53cb26c054cadb5d7b" +checksum = "c38332c9f4978f36ea3a6ada645c734b73796c8af16fdabebff35792b74150f7" dependencies = [ "agave-feature-set", "agave-transaction-view", @@ -10269,9 +10261,9 @@ checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" [[package]] name = "solana-sbpf" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f84c593fa3d4131045b606dec5acf9d8eac73791bc786ca9911057aec8f43ec" +checksum = "d777d7a89267dd133e985113c7e7f820fb7cfd9123a4a350cf8b39ebae1920bc" dependencies = [ "byteorder", "combine 3.8.1", @@ -10281,7 +10273,7 @@ dependencies = [ "log 0.4.33", "rand 0.8.6", "rustc-demangle", - "thiserror 2.0.18", + "thiserror 2.0.20", "winapi 0.3.9", ] @@ -10352,7 +10344,7 @@ checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" dependencies = [ "libsecp256k1 0.6.0", "solana-define-syscall 2.3.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -10363,7 +10355,7 @@ checksum = "e3a1ad3ed7846631c88c71c5d2f21a2ecb6b61da333d9be173b6b061b35609ae" dependencies = [ "k256", "solana-define-syscall 5.1.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -10662,9 +10654,9 @@ dependencies = [ [[package]] name = "solana-stake-interface" -version = "3.1.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49eb5c77484214c3484921e2cdda79d185373118b5458c1b2df0f1a04c3bc30" +checksum = "252b4291eb25a5a356149ed4d4a940d5f655186210fa84b5d0d8d55c3dd42a81" dependencies = [ "num-traits", "serde", @@ -10681,9 +10673,9 @@ dependencies = [ [[package]] name = "solana-streamer" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50041e23b6a0677907d697c0e22a1c838590f51522faa5621311cca853348823" +checksum = "3383a0d01e0731c3cf3e6bfdef6928d56c0a604db39adc7256b4f1e643715d11" dependencies = [ "agave-xdp", "bytes 1.12.1", @@ -10697,12 +10689,12 @@ dependencies = [ "nix", "num_cpus", "pem", - "percentage", "quinn", "rand 0.9.4", "rustls 0.23.41", "smallvec", "solana-keypair", + "solana-measure", "solana-metrics", "solana-net-utils", "solana-packet", @@ -10712,27 +10704,28 @@ dependencies = [ "solana-time-utils", "solana-tls-utils", "solana-transaction-error 3.3.1", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-util 0.7.18", ] [[package]] name = "solana-svm" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0567a00d15084c3ff2ad3d35d0faa4caeb7131dbff146af26bdcffb837e96baf" +checksum = "41e8ae0a5d0798c1ec33419b0ef18af83fd33e4574986baf59754f095035abc9" dependencies = [ "ahash", "log 0.4.33", "percentage", "serde", "solana-account 4.3.1", + "solana-bpf-loader-program", "solana-clock 3.1.1", "solana-fee-structure", "solana-hash 4.5.0", "solana-instruction 3.4.0", - "solana-instructions-sysvar 3.0.0", + "solana-instructions-sysvar 4.0.0", "solana-loader-v3-interface 7.0.0", "solana-loader-v4-interface 3.1.0", "solana-message 4.3.0", @@ -10756,14 +10749,14 @@ dependencies = [ "solana-transaction-context", "solana-transaction-error 3.3.1", "spl-generic-token", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "solana-svm-callback" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d50ab466a202b3ec768d0d1f808c570822b4d2492801772359b9509baf1e8c6" +checksum = "3603ca4c273926155ace95887b30cd1e3918835737687bffa8a5d8749dcf0e91" dependencies = [ "solana-account 4.3.1", "solana-clock 3.1.1", @@ -10773,30 +10766,30 @@ dependencies = [ [[package]] name = "solana-svm-feature-set" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35320ec1b4460e2f748c9ebb6c42eb3069f8d85ec46a185ae9b5628430f3c606" +checksum = "bda42b13c4748e47af89010e16a4391e1901be860beb93eedc0ccab66795fe50" [[package]] name = "solana-svm-log-collector" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ba1a825b27776f6a92fae44613d5e7f906f0c56e542bbbe1a6746eb168daff" +checksum = "eec82e47b4ee1628f6f453f8ce8de5473458470fe60965e4d3be31fa82afdbea" dependencies = [ "log 0.4.33", ] [[package]] name = "solana-svm-measure" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ec9c705bec0fc99b9220e62fbae9fb0a297b9a95ff411badce934103a92ec7" +checksum = "6b309b60870dc1821d363807cdd40097d52db1529aae04344f361995bb88b084" [[package]] name = "solana-svm-timings" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b72896172acdbce474662e876fdd413092893599156500cc68840abea649441" +checksum = "44b30b0c61db8d711327c49ab2171984d564a93835ed77acb6aa2da0ecf434ba" dependencies = [ "eager", "enum-iterator", @@ -10805,9 +10798,9 @@ dependencies = [ [[package]] name = "solana-svm-transaction" -version = "4.1.2" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998e6d7d1197d29a77d3701ae273cfc3e95e79978546faa4d28af637ef5211e7" +checksum = "736d2cec944c6f8f298a7bd8ada5eb318cca9ab859281b1300842fa0e9a25afa" dependencies = [ "solana-hash 4.5.0", "solana-message 4.3.0", @@ -10819,25 +10812,24 @@ dependencies = [ [[package]] name = "solana-svm-type-overrides" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128cad78148dbd90e61a0bdf54706a24c356e858d85f2674dd7755eac594a4df" +checksum = "9b6cc834d4a01719767e68b9d30c33c4162b2d818afc084cf310715ea087e64f" dependencies = [ "rand 0.9.4", ] [[package]] name = "solana-syscalls" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2788d359af16fac879c081bc0b725e8a200702156af92e75e2c18346fcde332" +checksum = "6976ef46df8e2fd9ce7cc6396a6cf29bcc37c882def849e7d6bc48bd621c88f8" dependencies = [ "bincode", "libsecp256k1 0.7.2", "num-traits", "solana-account 4.3.1", "solana-account-info 3.1.1", - "solana-big-mod-exp 3.0.0", "solana-blake3-hasher 3.1.0", "solana-bls12-381-syscall", "solana-bn254", @@ -10858,14 +10850,15 @@ dependencies = [ "solana-sha256-hasher 3.1.0", "solana-sha512-hasher", "solana-stable-layout 3.0.1", - "solana-stake-interface 3.1.0", + "solana-stake-interface 4.3.0", "solana-svm-feature-set", "solana-svm-log-collector", "solana-svm-type-overrides", "solana-sysvar 4.1.0", "solana-sysvar-id 3.1.0", "solana-transaction-context", - "thiserror 2.0.18", + "thiserror 2.0.20", + "wincode", ] [[package]] @@ -10917,9 +10910,9 @@ dependencies = [ [[package]] name = "solana-system-program" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64154747d8e97a2ac1869e7633b9d5b8cac01d1ca99cf05f5910eb4b5718fbbc" +checksum = "ba583e7a473426fab4c0b17fde848a6bb798b1ede5a14c4f13a3494a2e748fc6" dependencies = [ "bincode", "log 0.4.33", @@ -11054,10 +11047,11 @@ checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" [[package]] name = "solana-tls-utils" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d2a545898c0d8ec87106e10dc93779fd8b222b4640e7060292140a8f133ca4" +checksum = "7c4e62f076680c61c2e503703c86d11437523c30c78c0442e41d4288a06aa9f9" dependencies = [ + "quinn", "rustls 0.23.41", "solana-keypair", "solana-pubkey 4.2.0", @@ -11067,9 +11061,9 @@ dependencies = [ [[package]] name = "solana-tpu-client" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351f0ef9c7a4aaa17fd9f01ad46e2700e5391b322151d37886078f3a3cf899fc" +checksum = "c3fb67f45bf406f57032181d134c6a7134bb433c19497ae1d01948625d85fa66" dependencies = [ "futures-util", "indicatif", @@ -11080,6 +11074,7 @@ dependencies = [ "solana-commitment-config", "solana-connection-cache", "solana-epoch-schedule 3.2.0", + "solana-leader-schedule", "solana-message 4.3.0", "solana-pubkey 4.2.0", "solana-pubsub-client", @@ -11089,7 +11084,7 @@ dependencies = [ "solana-signer", "solana-transaction 4.1.5", "solana-transaction-error 3.3.1", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "wincode", ] @@ -11140,15 +11135,15 @@ dependencies = [ [[package]] name = "solana-transaction-context" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffd061d881fb2c182078e6ca6094fe113e50edbec224c4f8999747eb643cd02" +checksum = "855e7e2bf38f3a8e18c9dc1f2a4d2fd947f52c81f4e3b73077a3827582be6e03" dependencies = [ "bincode", "serde", "solana-account 4.3.1", "solana-instruction 3.4.0", - "solana-instructions-sysvar 3.0.0", + "solana-instructions-sysvar 4.0.0", "solana-pubkey 4.2.0", "solana-rent 4.3.0", "solana-sbpf", @@ -11179,9 +11174,9 @@ dependencies = [ [[package]] name = "solana-transaction-status" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4704225bedadc396d0f8cfb64022318f552b1ad73b645e7651bfffe736557060" +checksum = "fe9e6f62b08de0b5b30eb04303f737510f7a9429a08f7f3c7e9c849951f85ce4" dependencies = [ "Inflector", "agave-reserved-account-keys", @@ -11204,27 +11199,28 @@ dependencies = [ "solana-reward-info", "solana-sdk-ids 3.1.0", "solana-signature", - "solana-stake-interface 3.1.0", + "solana-stake-interface 4.3.0", "solana-system-interface 3.2.0", "solana-transaction 4.1.5", "solana-transaction-error 3.3.1", "solana-transaction-status-client-types", "solana-vote-interface 6.0.3", + "solana-zk-sdk-pod", "spl-associated-token-account-interface", "spl-memo-interface", - "spl-token-2022-interface", + "spl-token-2022-interface 3.1.1", "spl-token-group-interface", - "spl-token-interface 2.0.0", - "spl-token-metadata-interface", - "thiserror 2.0.18", + "spl-token-interface 3.0.0", + "spl-token-metadata-interface 1.0.1", + "thiserror 2.0.20", "wincode", ] [[package]] name = "solana-transaction-status-client-types" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be5aafaae40f808411265e0de673c1dc2b8a68bf4ad4d38ba7e141d5232a5f23" +checksum = "ffbd5a3d85ae247b69fa21578fdd9fbd965345390fddb60fe3586542e30ff98c" dependencies = [ "base64 0.22.1", "bincode", @@ -11240,15 +11236,15 @@ dependencies = [ "solana-transaction 4.1.5", "solana-transaction-context", "solana-transaction-error 3.3.1", - "thiserror 2.0.18", + "thiserror 2.0.20", "wincode", ] [[package]] name = "solana-udp-client" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99256a14a632f4d924ba24f1a2e82ea71ce0f09fcb553229d7dac99dffb40cf8" +checksum = "0c46127b2df6bbdaf2412134dff29228665611fd267beebee34e1d4e226f375d" dependencies = [ "async-trait", "solana-connection-cache", @@ -11260,9 +11256,9 @@ dependencies = [ [[package]] name = "solana-unified-scheduler-logic" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bd96244fd2dbf5e0e99472fed09e812bcee971fe3bc341eb622acd7a21f670" +checksum = "06f57ad7cf6cc429888e5e6be029e25dac9fc4ca97fb93da0a36ec46e3d37a43" dependencies = [ "assert_matches", "solana-clock 3.1.1", @@ -11276,9 +11272,9 @@ dependencies = [ [[package]] name = "solana-version" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33921090d55beb8dcded76d80982dd4eb02c8bc8115bad8a9aeb901531c10df" +checksum = "dcc9449982ced50bb21dc4ec401bb75db2e21880980b7c8506811d2ae473d130" dependencies = [ "agave-feature-set", "rand 0.9.4", @@ -11286,13 +11282,15 @@ dependencies = [ "serde", "solana-sanitize 3.0.1", "solana-serde-varint 3.0.1", + "solana-wincode-varint", + "wincode", ] [[package]] name = "solana-vote" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18ad40c13ae8ea778ec574afbe6327f0aa331d4923d5707b8a3583af00b2efd1" +checksum = "516638bd33b95a9454f32bf13080058d5bb1881f288fb36f727951be806d0b88" dependencies = [ "log 0.4.33", "serde", @@ -11311,7 +11309,7 @@ dependencies = [ "solana-svm-transaction", "solana-transaction 4.1.5", "solana-vote-interface 6.0.3", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -11366,9 +11364,9 @@ dependencies = [ [[package]] name = "solana-vote-program" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d772aa219b97b0cd9ec512c9330179714e3bb6231df4a8b81981b6c88f85f1" +checksum = "7730ce2760d827ae57ef9b525edb5c65e046373c1a76a2f03d1bab70cbe4feec" dependencies = [ "agave-feature-set", "bincode", @@ -11391,6 +11389,15 @@ dependencies = [ "solana-vote-interface 6.0.3", ] +[[package]] +name = "solana-wincode-varint" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6679e72a01dcfe7ff180f0e9d3a0d165e4e9664426930014f2702e7571c259c7" +dependencies = [ + "wincode", +] + [[package]] name = "solana-zero-copy" version = "1.1.1" @@ -11402,11 +11409,27 @@ dependencies = [ "bytemuck_derive", ] +[[package]] +name = "solana-zk-elgamal-proof-interface" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8da7f01db2148a1dc16261ff1dc6f3930a1e255a33cece4f1b56658694f27f7" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "solana-address 2.6.1", + "solana-instruction 3.4.0", + "solana-sdk-ids 3.1.0", + "solana-zk-sdk-pod", +] + [[package]] name = "solana-zk-elgamal-proof-program" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d841aa9791a275535f676c9403413c66da7c1bb1c867d3575b58f4e5b881ef" +checksum = "816ee19bc9c5f7700c6153ebc513f3bacc71645bd174f78b73f66c9acf6e9485" dependencies = [ "bytemuck", "solana-instruction 3.4.0", @@ -11448,7 +11471,7 @@ dependencies = [ "solana-signature", "solana-signer", "subtle", - "thiserror 2.0.18", + "thiserror 2.0.20", "wasm-bindgen", "zeroize", ] @@ -11483,15 +11506,61 @@ dependencies = [ "solana-signature", "solana-signer", "subtle", - "thiserror 2.0.18", + "thiserror 2.0.20", + "zeroize", +] + +[[package]] +name = "solana-zk-sdk" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96cd8535d2f40d43a1d47af2849e1c86706a597a2ad84207441f51a50f09d8fd" +dependencies = [ + "aes-gcm-siv", + "base64 0.22.1", + "bincode", + "bytemuck", + "curve25519-dalek 4.1.3", + "hkdf", + "itertools 0.14.0", + "merlin", + "rand 0.8.6", + "serde", + "serde_derive", + "serde_json", + "sha2 0.10.9", + "sha3", + "solana-address 2.6.1", + "solana-derivation-path", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-signature", + "solana-signer", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", + "subtle", + "thiserror 2.0.20", "zeroize", ] +[[package]] +name = "solana-zk-sdk-pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a800583b7a4cea3e851686af162cc6e4712eef97fa91dfa9aca2459b95c84777" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "bytemuck_derive", + "solana-nullable", + "thiserror 2.0.20", +] + [[package]] name = "solana-zk-token-proof-program" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4841d6e055847c84d011d187cbad022ea4f8832c85a61a1aa220c192bd4dfd0" +checksum = "2e81d7b076afb34121bb72f0d522cf4ff84a4adebd29ba0d73ae428299b60e5e" dependencies = [ "solana-program-runtime", ] @@ -11578,12 +11647,12 @@ dependencies = [ [[package]] name = "spl-memo-interface" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" +checksum = "3745d384b0afee980d43d62b66c27bdcbbd03507732b8d3626d3413cb72084f2" dependencies = [ "solana-instruction 3.4.0", - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", ] [[package]] @@ -11603,7 +11672,7 @@ dependencies = [ "solana-pubkey 3.0.0", "solana-zero-copy", "solana-zk-sdk 4.0.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -11626,12 +11695,42 @@ dependencies = [ "solana-sdk-ids 3.1.0", "solana-zk-sdk 4.0.0", "spl-pod", - "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-extraction 0.5.1", "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface", - "spl-token-metadata-interface", + "spl-token-metadata-interface 0.8.0", + "spl-type-length-value", + "thiserror 2.0.20", +] + +[[package]] +name = "spl-token-2022-interface" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "821d96d034ea31c4965d182c742153c491ae0abee531331b55771086c5030d86" +dependencies = [ + "arrayref", + "bytemuck", + "getrandom 0.2.17", + "num-derive", + "num-traits", + "num_enum", + "solana-account-info 3.1.1", + "solana-address 2.6.1", + "solana-instruction 3.4.0", + "solana-nullable", + "solana-program-error 3.0.1", + "solana-program-option 3.1.0", + "solana-program-pack 3.1.0", + "solana-sdk-ids 3.1.0", + "solana-zero-copy", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", + "spl-token-confidential-transfer-proof-extraction 0.6.0", + "spl-token-group-interface", + "spl-token-metadata-interface 1.0.1", "spl-type-length-value", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -11651,7 +11750,27 @@ dependencies = [ "solana-sdk-ids 3.1.0", "solana-zk-sdk 4.0.0", "spl-pod", - "thiserror 2.0.18", + "thiserror 2.0.20", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd536b30c532568fad8430077875c3abc16d365e464ebfa2902bc65cb91bdc4" +dependencies = [ + "bytemuck", + "solana-account-info 3.1.1", + "solana-address 2.6.1", + "solana-curve25519 3.1.14", + "solana-instruction 3.4.0", + "solana-instructions-sysvar 3.0.0", + "solana-msg 3.1.0", + "solana-program-error 3.0.1", + "solana-sdk-ids 3.1.0", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", + "thiserror 2.0.20", ] [[package]] @@ -11662,7 +11781,7 @@ checksum = "a0cd59fce3dc00f563c6fa364d67c3f200d278eae681f4dc250240afcfe044b1" dependencies = [ "curve25519-dalek 4.1.3", "solana-zk-sdk 4.0.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -11681,7 +11800,7 @@ dependencies = [ "solana-program-error 3.0.1", "solana-zero-copy", "spl-discriminator", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -11701,7 +11820,7 @@ dependencies = [ "solana-program-pack 3.1.0", "solana-pubkey 3.0.0", "solana-sdk-ids 3.1.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -11721,7 +11840,7 @@ dependencies = [ "solana-program-pack 3.1.0", "solana-pubkey 3.0.0", "solana-sdk-ids 3.1.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -11740,7 +11859,27 @@ dependencies = [ "spl-discriminator", "spl-pod", "spl-type-length-value", - "thiserror 2.0.18", + "thiserror 2.0.20", +] + +[[package]] +name = "spl-token-metadata-interface" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3d96f175e7022ff200464dfa75a3708a4e9b70c83c4ecd04fe52ee479f4fef" +dependencies = [ + "borsh 1.7.0", + "num-derive", + "num-traits", + "num_enum", + "solana-address 2.6.1", + "solana-borsh 3.0.2", + "solana-instruction 3.4.0", + "solana-nullable", + "solana-program-error 3.0.1", + "spl-discriminator", + "spl-type-length-value", + "thiserror 2.0.20", ] [[package]] @@ -11757,7 +11896,7 @@ dependencies = [ "solana-program-error 3.0.1", "solana-zero-copy", "spl-discriminator", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -12033,16 +12172,19 @@ dependencies = [ "solana-transaction-context", "solana-transaction-error 3.3.1", "solana-transaction-status", + "solana-transaction-status-client-types", "solana-version", + "solana-zk-sdk 7.0.1", + "solana-zk-sdk-pod", "spl-associated-token-account-interface", - "spl-token-2022-interface", - "spl-token-interface 2.0.0", - "spl-token-metadata-interface", + "spl-token-2022-interface 3.1.1", + "spl-token-interface 3.0.0", + "spl-token-metadata-interface 1.0.1", "surfpool-db", "surfpool-types", "tempfile", "test-case", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "txtx-addon-kit", "txtx-addon-network-svm", @@ -12080,7 +12222,7 @@ dependencies = [ "solana-pubkey 3.0.0", "solana-signer", "spl-associated-token-account-interface", - "spl-token-interface 2.0.0", + "spl-token-interface 3.0.0", "surfpool-core", "surfpool-types", "tokio", @@ -12114,11 +12256,11 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-transaction 4.1.5", "spl-associated-token-account-interface", - "spl-token-interface 2.0.0", + "spl-token-interface 3.0.0", "surfpool-core", "surfpool-types", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "uuid", ] @@ -12195,7 +12337,7 @@ dependencies = [ "solana-transaction-error 3.3.1", "solana-transaction-status", "test-case", - "thiserror 2.0.18", + "thiserror 2.0.20", "ts-rs", "txtx-addon-kit", "txtx-addon-network-svm-types", @@ -12231,6 +12373,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -12382,11 +12535,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.20", ] [[package]] @@ -12402,13 +12555,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.4", ] [[package]] @@ -12815,7 +12968,7 @@ version = "12.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.20", "ts-rs-macros", ] @@ -12860,7 +13013,7 @@ dependencies = [ "rustls 0.23.41", "rustls-pki-types", "sha1 0.10.6", - "thiserror 2.0.18", + "thiserror 2.0.20", "utf-8", "webpki-roots 0.26.11", ] @@ -12935,7 +13088,7 @@ dependencies = [ "solana-transaction 4.1.5", "solana_idl", "spl-associated-token-account-interface", - "spl-token-2022-interface", + "spl-token-2022-interface 2.1.0", "spl-token-interface 2.0.0", "tiny-bip39", "txtx-addon-kit", @@ -13470,7 +13623,7 @@ dependencies = [ "pastey", "proc-macro2", "quote", - "thiserror 2.0.18", + "thiserror 2.0.20", "wincode-derive", ] @@ -13912,7 +14065,7 @@ dependencies = [ "nom 7.1.3", "oid-registry", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", ] diff --git a/Cargo.toml b/Cargo.toml index b62e882b0..8963c5889 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,11 +27,11 @@ resolver = "2" [workspace.dependencies] actix-cors = "0.7.0" actix-web = { version = "4", default-features = false } -agave-feature-set = { version = "4.0", default-features = false, features = [ +agave-feature-set = { version = "4.2", default-features = false, features = [ "agave-unstable-api", ] } -agave-geyser-plugin-interface = { version = "4.0", default-features = false } -agave-reserved-account-keys = { version = "4.0", default-features = false } +agave-geyser-plugin-interface = { version = "4.2", default-features = false } +agave-reserved-account-keys = { version = "4.2", default-features = false } anchor-lang-idl = "0.1.2" ansi_term = "0.12.1" anyhow = { version = "1.0.95", default-features = false } @@ -85,8 +85,8 @@ juniper_codegen = { version = "0.16.0", default-features = false } juniper_graphql_ws = { version = "0.4.0", default-features = false } lazy_static = "1.5.0" libloading = "0.7.4" -litesvm = { version = "0.14.0", features = ["nodejs-internal", "precompiles"] } -litesvm-token = "0.14.0" +litesvm = { version = "0.16.0", features = ["nodejs-internal", "precompiles"] } +litesvm-token = { version = "0.16.0" } log = "0.4.27" mime_guess = { version = "2.0.4", default-features = false } mustache = "0.9.0" @@ -110,23 +110,23 @@ strum = { version = "0.26", default-features = false, features = ["derive"] } solana-account = { version = "4.3", default-features = false } solana-account-decoder = { version = "4.0", default-features = false } solana-account-decoder-client-types = { version = "4.0", default-features = false } -solana-address-lookup-table-interface = { version = "3.0", default-features = false } +solana-address-lookup-table-interface = { version = "~3.1", default-features = false } solana-client = { version = "4.0", default-features = false } -solana-clock = { version = "3.0", default-features = false } +solana-clock = { version = "~3.1", default-features = false } solana-commitment-config = { version = "3.1", default-features = false } -solana-compute-budget-interface = { version = "3.0", default-features = false } +solana-compute-budget-interface = { version = "~3.0", default-features = false } solana-epoch-info = { version = "3.1", default-features = false } -solana-epoch-schedule = { version = "3.0", default-features = false } +solana-epoch-schedule = { version = "~3.2", default-features = false } solana-ed25519-program = { version = "3.0", default-features = false } solana-feature-gate-interface = { version = "3.1", default-features = false } solana-genesis-config = { version = "4.0", default-features = false } -solana-hash = { version = "4.2", default-features = false } +solana-hash = { version = "~4.5", default-features = false } solana-inflation = { version = "3.0", default-features = false } -solana-instruction = { version = "3.2", default-features = false } +solana-instruction = { version = "~3.4", default-features = false } solana-keypair = { version = "3.1", default-features = false } solana-loader-v3-interface = { version = "6.1", default-features = false } solana-message = { version = "4.3", default-features = false } -solana-nonce = { version = "3.0", default-features = false } +solana-nonce = { version = "~3.2", default-features = false } solana-packet = { version = "4.0", default-features = false } solana-program-option = { version = "3.0", default-features = false } solana-program-pack = { version = "3.1", default-features = false } @@ -134,25 +134,30 @@ solana-pubsub-client = { version = "4.0", default-features = false } solana-pubkey = { version = "3.0", default-features = false } solana-rpc-client = { version = "4.0", default-features = false } solana-rpc-client-api = { version = "4.0", default-features = false } -solana-runtime = { version = "4.0", default-features = false, features = ["agave-unstable-api"] } +solana-runtime = { version = "4.2", default-features = false, features = ["agave-unstable-api"] } solana-sdk-ids = { version = "3.1", default-features = false } -solana-signature = { version = "3.3", default-features = false, features = [ +solana-signature = { version = "~3.4", default-features = false, features = [ "rand", ] } solana-signer = { version = "3.0", default-features = false } -solana-slot-hashes = { version = "3.0", default-features = false } +solana-slot-hashes = { version = "~3.1", default-features = false } solana-system-interface = { version = "3.0", default-features = false } -solana-sysvar = { version = "4.1", default-features = false, features = ["bincode", "wincode"] } +solana-sysvar = { version = "~4.1", default-features = false, features = ["bincode", "wincode"] } solana-sysvar-id = { version = "3.1", default-features = false } -solana-transaction = { version = "4.1", default-features = false, features = ["serde", "wincode"] } -solana-transaction-context = { version = "4.0", default-features = false } -solana-transaction-error = { version = "3.1", default-features = false } -solana-transaction-status = { version = "4.1", default-features = false, features = ["agave-unstable-api"] } +solana-transaction = { version = "~4.1", default-features = false, features = ["serde", "wincode"] } +solana-transaction-context = { version = "4.2", default-features = false } +solana-transaction-error = { version = "~3.3", default-features = false } +solana-transaction-status = { version = "4.2", default-features = false, features = ["agave-unstable-api"] } +solana-transaction-status-client-types = { version = "4.2", default-features = false } solana-version = { version = "4.0", default-features = false } +# spl-token-2022-interface 3.x stopped re-exporting these; the pod types moved +# out of solana-zk-sdk into solana-zk-sdk-pod, which 3.1.1 depends on directly. +solana-zk-sdk = { version = "7.0", default-features = false } +solana-zk-sdk-pod = { version = "0.1.2", default-features = false } spl-associated-token-account-interface = { version = "2.0.0", default-features = false } -spl-token-2022-interface = { version = "2.0.0", default-features = false } -spl-token-interface = { version = "2.0.0", default-features = false } -spl-token-metadata-interface = { version = "0.8.0", default-features = false } +spl-token-2022-interface = { version = "3.1.1", default-features = false } +spl-token-interface = { version = "3.0.0", default-features = false } +spl-token-metadata-interface = { version = "1.0.0", default-features = false } tempfile = "3.23.0" test-case = "^3.3.1" ts-rs = { version = "=12.0.1" } @@ -181,7 +186,7 @@ txtx-core = { version = "0.4.18" } txtx-gql = { version = "0.3.11" } txtx-supervisor-ui = { version = "0.2.11", default-features = false } -# [patch.crates-io] +[patch.crates-io] # Local # txtx-addon-kit = { path = "../txtx/crates/txtx-addon-kit" } # txtx-core = { path = "../txtx/crates/txtx-core" } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index dc64e9a61..48a18662e 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -85,7 +85,10 @@ solana-transaction = { workspace = true } solana-transaction-context = { workspace = true } solana-transaction-error = { workspace = true } solana-transaction-status = { workspace = true } +solana-transaction-status-client-types = { workspace = true } solana-version = { workspace = true } +solana-zk-sdk = { workspace = true } +solana-zk-sdk-pod = { workspace = true } spl-associated-token-account-interface = { workspace = true } spl-token-interface = { workspace = true } spl-token-2022-interface = { workspace = true } diff --git a/crates/core/src/rpc/full.rs b/crates/core/src/rpc/full.rs index 4238638c5..333924a17 100644 --- a/crates/core/src/rpc/full.rs +++ b/crates/core/src/rpc/full.rs @@ -2756,8 +2756,10 @@ mod tests { use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_message::{ - MessageHeader, legacy::Message as LegacyMessage, v0::Message as V0Message, - v1::MAX_TRANSACTION_SIZE, + MessageHeader, + legacy::Message as LegacyMessage, + v0::Message as V0Message, + v1::{MAX_TRANSACTION_SIZE, Message as V1Message, TransactionConfig}, }; use solana_pubkey::Pubkey; use solana_signer::Signer; @@ -2774,6 +2776,7 @@ mod tests { EncodedTransaction, EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiMessage, UiRawMessage, UiTransaction, UiTransactionEncoding, }; + use solana_transaction_status_client_types::UiTransactionConfig; use surfpool_types::{SimnetCommand, TransactionConfirmationStatus}; use test_case::test_case; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -2785,6 +2788,26 @@ mod tests { types::{SyntheticBlockhash, TransactionWithStatusMeta}, }; + fn build_v1_transaction( + payer: &Pubkey, + signers: &[&Keypair], + instructions: &[Instruction], + recent_blockhash: &Hash, + ) -> VersionedTransaction { + let msg = VersionedMessage::V1( + V1Message::try_compile_with_config( + &payer, + instructions, + *recent_blockhash, + TransactionConfig::empty() + .with_compute_unit_limit(20_000) + .with_loaded_accounts_data_size_limit(20_000), + ) + .unwrap(), + ); + VersionedTransaction::try_new(msg, signers).unwrap() + } + fn build_v0_transaction( payer: &Pubkey, signers: &[&Keypair], @@ -2823,7 +2846,7 @@ mod tests { .rpc .send_transaction( Some(setup_clone.context), - bs58::encode(bincode::serialize(&tx).unwrap()).into_string(), + bs58::encode(wincode::serialize(&tx).unwrap()).into_string(), None, ) .unwrap(); @@ -3303,6 +3326,7 @@ mod tests { #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")] #[test_case(TransactionVersion::Number(0) ; "V0 transactions")] + #[test_case(TransactionVersion::Number(1) ; "V1 transactions")] #[tokio::test(flavor = "multi_thread")] async fn test_send_transaction(version: TransactionVersion) { let payer = Keypair::new(); @@ -3335,6 +3359,16 @@ mod tests { )], &recent_blockhash, ), + TransactionVersion::Number(1) => build_v1_transaction( + &payer.pubkey(), + &[&payer.insecure_clone()], + &[system_instruction::transfer( + &payer.pubkey(), + &pk, + LAMPORTS_PER_SOL, + )], + &recent_blockhash, + ), _ => unimplemented!(), }; @@ -3356,6 +3390,7 @@ mod tests { #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")] #[test_case(TransactionVersion::Number(0) ; "V0 transactions")] + #[test_case(TransactionVersion::Number(1) ; "V1 transactions")] #[tokio::test(flavor = "multi_thread")] async fn test_simulate_transaction(version: TransactionVersion) { let payer = Keypair::new(); @@ -3390,6 +3425,12 @@ mod tests { &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)], &recent_blockhash, ), + TransactionVersion::Number(1) => build_v1_transaction( + &payer.pubkey(), + &[&payer.insecure_clone()], + &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)], + &recent_blockhash, + ), _ => unimplemented!(), }; @@ -3397,7 +3438,7 @@ mod tests { .rpc .simulate_transaction( Some(setup.context), - bs58::encode(bincode::serialize(&tx).unwrap()).into_string(), + bs58::encode(wincode::serialize(&tx).unwrap()).into_string(), Some(RpcSimulateTransactionConfig { sig_verify: true, replace_recent_blockhash: false, @@ -3425,7 +3466,7 @@ mod tests { data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64), owner: system_program::id().to_string(), executable: false, - rent_epoch: 0, + rent_epoch: 18446744073709551615, space: Some(0), })]), "Wrong account content" @@ -3522,7 +3563,7 @@ mod tests { data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64), owner: system_program::id().to_string(), executable: false, - rent_epoch: 0, + rent_epoch: 18446744073709551615, space: Some(0), })]), "Wrong account content" @@ -3590,6 +3631,7 @@ mod tests { #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")] #[test_case(TransactionVersion::Number(0) ; "V0 transactions")] + #[test_case(TransactionVersion::Number(1) ; "V1 transactions")] #[tokio::test(flavor = "multi_thread")] async fn test_simulate_transaction_replace_recent_blockhash(version: TransactionVersion) { let payer = Keypair::new(); @@ -3629,6 +3671,12 @@ mod tests { &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)], &recent_blockhash, ), + TransactionVersion::Number(1) => build_v1_transaction( + &payer.pubkey(), + &[&payer.insecure_clone()], + &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)], + &recent_blockhash, + ), _ => unimplemented!(), }; tx.message.set_recent_blockhash(bad_blockhash); @@ -3666,7 +3714,7 @@ mod tests { .rpc .simulate_transaction( Some(setup.context), - bs58::encode(bincode::serialize(&tx).unwrap()).into_string(), + bs58::encode(wincode::serialize(&tx).unwrap()).into_string(), Some(valid_config), ) .await @@ -3761,6 +3809,7 @@ mod tests { #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")] #[test_case(TransactionVersion::Number(0) ; "V0 transactions")] + #[test_case(TransactionVersion::Number(1) ; "V1 transactions")] #[tokio::test(flavor = "multi_thread")] async fn test_get_transaction(version: TransactionVersion) { let payer = Keypair::new(); @@ -3795,6 +3844,12 @@ mod tests { &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)], &recent_blockhash, ), + TransactionVersion::Number(1) => build_v1_transaction( + &payer.pubkey(), + &[&payer.insecure_clone()], + &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)], + &recent_blockhash, + ), _ => unimplemented!(), }; @@ -3853,7 +3908,15 @@ mod tests { VersionedMessage::V0(_) => Some(vec![]), VersionedMessage::V1(_) => None, }, - transaction_config: None, + transaction_config: match tx.message { + VersionedMessage::Legacy(_) | VersionedMessage::V0(_) => None, + VersionedMessage::V1(_) => Some(UiTransactionConfig { + priority_fee: None, + compute_unit_limit: Some(20_000), + loaded_accounts_data_size_limit: Some(20_000), + heap_size: None + }), + }, }) }), meta: res.transaction.clone().meta, // Using the same values to avoid reintroducing processing logic errors diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index 7f632e86d..445aaf5ca 100644 --- a/crates/core/src/rpc/surfnet_cheatcodes.rs +++ b/crates/core/src/rpc/surfnet_cheatcodes.rs @@ -1285,23 +1285,19 @@ pub trait SurfnetCheatcodes { keys: ConfidentialBalanceKeys, ) -> BoxFuture>>; - /// A cheat code to derive an owner's confidential-transfer keys from the owner's signatures. + /// A cheat code to derive an owner's confidential-transfer keys from the owner's signature. /// /// The confidential cheatcodes take an `elgamalPubkey` and an `aesKey`, which a client would /// normally derive with an external confidential-transfer SDK. Deriving them here lets a test /// drive the whole confidential suite with no client-side crypto dependency. /// /// ## Parameters - /// - `elgamal_signature`: The owner's 64-byte signature over the bytes `"ElGamalSecretKey"` - /// followed by the token account address, base58 or base64 encoded. - /// - `ae_signature`: The owner's 64-byte signature over the bytes `"AeKey"` followed by the token - /// account address, base58 or base64 encoded. + /// - `signature`: The owner's 64-byte signature over `"solana-conf-bal/v1"` followed by the + /// desired public seed (normally the token account address), base58 or base64 encoded. /// - /// `solana_zk_sdk` derives the two keys from signatures over two different domain-separated - /// messages, so one signature cannot reproduce both: passing the same signature twice still - /// returns a well-formed pair, just not the pair the signer path derives. Signing those two - /// messages over the token account address is what scopes the keys to that account; the caller - /// picks the seed, so per-wallet keying is the same call with a different message signed. + /// `solana_zk_sdk` derives both keys from this single signature using its canonical HKDF-SHA512 + /// derivation. Signing the message with the token account address as its seed scopes the keys + /// to that account; the caller can use a different seed for per-wallet keying. /// /// ## Returns /// A `RpcResponse` with base58 `elgamalPubkey` (for @@ -1315,8 +1311,7 @@ pub trait SurfnetCheatcodes { /// "id": 1, /// "method": "surfnet_deriveConfidentialKeys", /// "params": [ - /// "", - /// "" + /// "" /// ] /// } /// ``` @@ -1341,13 +1336,13 @@ pub trait SurfnetCheatcodes { /// ``` /// /// # Notes - /// The owner's *signing* key never crosses the wire: the caller signs the two seed messages - /// locally and sends only the signatures, so a hardware signer, which never exposes its signing - /// key, can drive this. The signatures are used only as key material and are not stored. + /// The owner's *signing* key never crosses the wire: the caller signs the canonical seed message + /// locally and sends only the signature, so a hardware signer, which never exposes its signing + /// key, can drive this. The signature is used only as key material and is not stored. /// /// What that does and does not buy is worth stating plainly. Because the ElGamal secret is a - /// hash of the signature, `elgamalSignature` is exactly as sensitive as the `elgamalSecretKey` - /// it derives: anyone who sees it recomputes that key offline. + /// HKDF input, `signature` is exactly as sensitive as the derived confidential keys: anyone who + /// sees it recomputes them offline. /// /// The derived `elgamalSecretKey` and `aesKey` do travel back in the response, and /// `surfnet_getConfidentialBalance` takes them back as inputs. That is the point of the @@ -1358,8 +1353,7 @@ pub trait SurfnetCheatcodes { fn derive_confidential_keys( &self, meta: Self::Metadata, - elgamal_signature: String, - ae_signature: String, + signature: String, ) -> Result>; /// A "cheat code" method for developers to write program data at a specified offset in Surfpool. @@ -2455,12 +2449,10 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { fn derive_confidential_keys( &self, meta: Self::Metadata, - elgamal_signature: String, - ae_signature: String, + signature: String, ) -> Result> { let svm_locker = meta.get_svm_locker()?; - let keys = derive_confidential_keys(&elgamal_signature, &ae_signature) - .map_err(Error::invalid_params)?; + let keys = derive_confidential_keys(&signature).map_err(Error::invalid_params)?; Ok(RpcResponse { context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()), value: keys, @@ -2616,7 +2608,7 @@ mod tests { use super::*; use crate::{ rpc::surfnet_cheatcodes::SurfnetCheatcodesRpc, tests::helpers::TestSetup, - types::confidential_key_signatures, + types::confidential_key_signature, }; /// Guards the canonical cheatcode method manifest in `surfpool-types` @@ -5320,16 +5312,14 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_set_confidential_token_account_spendable() { use bytemuck::bytes_of; - use spl_token_2022_interface::{ - extension::{ - BaseStateWithExtensions, StateWithExtensions, - confidential_transfer::ConfidentialTransferAccount, - }, - solana_zk_sdk::encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::ElGamalKeypair, - pod::elgamal::PodElGamalPubkey, - }, + use solana_zk_sdk::encryption::{ + auth_encryption::{AeCiphertext, AeKey}, + elgamal::ElGamalKeypair, + }; + use solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey; + use spl_token_2022_interface::extension::{ + BaseStateWithExtensions, StateWithExtensions, + confidential_transfer::ConfidentialTransferAccount, }; use surfpool_types::types::ConfidentialTransferAccountUpdate; @@ -5410,9 +5400,8 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_set_confidential_token_account_requires_aes_key() { use bytemuck::bytes_of; - use spl_token_2022_interface::solana_zk_sdk::encryption::{ - elgamal::ElGamalKeypair, pod::elgamal::PodElGamalPubkey, - }; + use solana_zk_sdk::encryption::elgamal::ElGamalKeypair; + use solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey; use surfpool_types::types::ConfidentialTransferAccountUpdate; let client = TestSetup::new(SurfnetCheatcodesRpc::empty()); @@ -5455,16 +5444,14 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_set_confidential_token_account_zero_balance_decrypts_to_zero() { use bytemuck::bytes_of; - use spl_token_2022_interface::{ - extension::{ - BaseStateWithExtensions, StateWithExtensions, - confidential_transfer::ConfidentialTransferAccount, - }, - solana_zk_sdk::encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::ElGamalKeypair, - pod::elgamal::PodElGamalPubkey, - }, + use solana_zk_sdk::encryption::{ + auth_encryption::{AeCiphertext, AeKey}, + elgamal::ElGamalKeypair, + }; + use solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey; + use spl_token_2022_interface::extension::{ + BaseStateWithExtensions, StateWithExtensions, + confidential_transfer::ConfidentialTransferAccount, }; use surfpool_types::types::ConfidentialTransferAccountUpdate; @@ -5548,14 +5535,10 @@ mod tests { &token_program, ); - let (elgamal_signature, ae_signature) = confidential_key_signatures(&owner, &token_account); + let signature = confidential_key_signature(&owner, &token_account); let keys = client .rpc - .derive_confidential_keys( - Some(client.context.clone()), - elgamal_signature, - ae_signature, - ) + .derive_confidential_keys(Some(client.context.clone()), signature) .expect("key derivation should succeed") .value; @@ -5612,12 +5595,11 @@ mod tests { #[test] fn test_confidential_pending_balance_recombines_lo_and_hi() { use bytemuck::bytes_of; - use spl_token_2022_interface::{ - extension::{ - BaseStateWithExtensionsMut, StateWithExtensionsMut, - confidential_transfer::ConfidentialTransferAccount, - }, - solana_zk_sdk::encryption::{elgamal::ElGamalKeypair, pod::elgamal::PodElGamalPubkey}, + use solana_zk_sdk::encryption::elgamal::ElGamalKeypair; + use solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey; + use spl_token_2022_interface::extension::{ + BaseStateWithExtensionsMut, StateWithExtensionsMut, + confidential_transfer::ConfidentialTransferAccount, }; use surfpool_types::types::ConfidentialTransferAccountUpdate; @@ -5699,15 +5681,10 @@ mod tests { let token_account = Pubkey::new_unique(); let derive = |token_account: Pubkey| { - let (elgamal_signature, ae_signature) = - confidential_key_signatures(&owner, &token_account); + let signature = confidential_key_signature(&owner, &token_account); client .rpc - .derive_confidential_keys( - Some(client.context.clone()), - elgamal_signature, - ae_signature, - ) + .derive_confidential_keys(Some(client.context.clone()), signature) .expect("key derivation should succeed") .value }; @@ -5730,14 +5707,10 @@ mod tests { &token_program, ); - let (elgamal_signature, ae_signature) = confidential_key_signatures(&owner, &token_account); + let signature = confidential_key_signature(&owner, &token_account); let keys = client .rpc - .derive_confidential_keys( - Some(client.context.clone()), - elgamal_signature, - ae_signature, - ) + .derive_confidential_keys(Some(client.context.clone()), signature) .expect("key derivation should succeed") .value; @@ -5776,15 +5749,10 @@ mod tests { // A wrong AES key must fail the ciphertext's authentication tag rather than // silently decrypt to some other number. - let (foreign_elgamal_signature, foreign_ae_signature) = - confidential_key_signatures(&Keypair::new(), &token_account); + let foreign_signature = confidential_key_signature(&Keypair::new(), &token_account); let wrong_keys = client .rpc - .derive_confidential_keys( - Some(client.context.clone()), - foreign_elgamal_signature, - foreign_ae_signature, - ) + .derive_confidential_keys(Some(client.context.clone()), foreign_signature) .expect("key derivation should succeed") .value; assert!( diff --git a/crates/core/src/rpc/utils.rs b/crates/core/src/rpc/utils.rs index fb53937d9..4db1d012c 100644 --- a/crates/core/src/rpc/utils.rs +++ b/crates/core/src/rpc/utils.rs @@ -305,12 +305,54 @@ pub fn adjust_default_transaction_config(config: &mut RpcTransactionConfig) { #[cfg(test)] mod tests { use solana_keypair::Keypair; - use solana_message::{MessageHeader, compiled_instruction::CompiledInstruction, v1}; + use solana_message::{ + MESSAGE_VERSION_PREFIX, MessageHeader, compiled_instruction::CompiledInstruction, legacy, + v0, v1, + }; use solana_signer::Signer; use solana_transaction::versioned::VersionedTransaction; use super::*; + fn signed_legacy_transaction(data_len: usize) -> VersionedTransaction { + let payer = Keypair::new(); + let message = VersionedMessage::Legacy(legacy::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + account_keys: vec![payer.pubkey(), Pubkey::new_unique()], + recent_blockhash: Hash::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![0; data_len], + }], + }); + VersionedTransaction::try_new(message, &[&payer]).unwrap() + } + + fn signed_v0_transaction(data_len: usize) -> VersionedTransaction { + let payer = Keypair::new(); + let message = VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + account_keys: vec![payer.pubkey(), Pubkey::new_unique()], + recent_blockhash: Hash::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![0; data_len], + }], + address_table_lookups: vec![], + }); + VersionedTransaction::try_new(message, &[&payer]).unwrap() + } + fn signed_v1_transaction(data_len: usize) -> VersionedTransaction { let payer = Keypair::new(); let message = VersionedMessage::V1(v1::Message::new( @@ -338,6 +380,22 @@ mod tests { transaction } + #[test] + fn uses_expected_wire_size_limits_by_transaction_version() { + let legacy_wire = wincode::serialize(&signed_legacy_transaction(0)).unwrap(); + assert_eq!(wire_size_limit(&legacy_wire), PACKET_DATA_SIZE); + assert_ne!(legacy_wire[0], V1_PREFIX); + + let v0_wire = wincode::serialize(&signed_v0_transaction(0)).unwrap(); + assert_eq!(wire_size_limit(&v0_wire), PACKET_DATA_SIZE); + assert_ne!(v0_wire[0], V1_PREFIX); + assert_eq!(v0_wire[1 + 64], MESSAGE_VERSION_PREFIX); + + let v1_wire = wincode::serialize(&signed_v1_transaction(0)).unwrap(); + assert_eq!(wire_size_limit(&v1_wire), MAX_TRANSACTION_SIZE); + assert_eq!(v1_wire[0], V1_PREFIX); + } + #[test] fn decodes_signed_v1_transaction_larger_than_legacy_packet() { let transaction = signed_v1_transaction(PACKET_DATA_SIZE); @@ -357,7 +415,27 @@ mod tests { } #[test] - fn rejects_oversized_v1_and_legacy_wire_payloads() { + fn rejects_oversized_legacy_v0_and_v1_wire_payloads() { + let oversized_legacy = wincode::serialize(&signed_legacy_transaction(PACKET_DATA_SIZE)) + .expect("legacy transaction should serialize"); + assert!(oversized_legacy.len() > PACKET_DATA_SIZE); + let legacy_error = decode_and_deserialize::( + BASE64_STANDARD.encode(oversized_legacy), + TransactionBinaryEncoding::Base64, + ) + .unwrap_err(); + assert!(legacy_error.message.contains("max: 1232 bytes")); + + let oversized_v0 = wincode::serialize(&signed_v0_transaction(PACKET_DATA_SIZE)) + .expect("v0 transaction should serialize"); + assert!(oversized_v0.len() > PACKET_DATA_SIZE); + let v0_error = decode_and_deserialize::( + BASE64_STANDARD.encode(oversized_v0), + TransactionBinaryEncoding::Base64, + ) + .unwrap_err(); + assert!(v0_error.message.contains("max: 1232 bytes")); + let mut oversized_v1 = vec![0; MAX_TRANSACTION_SIZE + 1]; oversized_v1[0] = V1_PREFIX; let v1_error = decode_and_deserialize::( @@ -366,14 +444,6 @@ mod tests { ) .unwrap_err(); assert!(v1_error.message.contains("max: 4096 bytes")); - - let oversized_legacy = vec![0; PACKET_DATA_SIZE + 1]; - let legacy_error = decode_and_deserialize::( - BASE64_STANDARD.encode(oversized_legacy), - TransactionBinaryEncoding::Base64, - ) - .unwrap_err(); - assert!(legacy_error.message.contains("max: 1232 bytes")); } #[test] diff --git a/crates/core/src/tests/integration.rs b/crates/core/src/tests/integration.rs index b3331813a..f4e9a5376 100644 --- a/crates/core/src/tests/integration.rs +++ b/crates/core/src/tests/integration.rs @@ -48,6 +48,7 @@ use solana_keypair::Keypair; use solana_message::{ AddressLookupTableAccount, Message, MessageHeader, VersionedMessage, legacy, v0::{self, MessageAddressTableLookup}, + v1::{self, TransactionConfig}, }; use solana_pubkey::Pubkey; use solana_pubsub_client::nonblocking::pubsub_client::PubsubClient; @@ -10777,18 +10778,16 @@ async fn test_token2022_metadata_realloc(test_type: TestType) { #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))] #[tokio::test(flavor = "multi_thread")] async fn test_confidential_balance_deposit_round_trip(test_type: TestType) { + use solana_zk_sdk::encryption::{ + auth_encryption::AeKey, + elgamal::{ElGamalKeypair, ElGamalPubkey}, + }; + use solana_zk_sdk_pod::encryption::auth_encryption::PodAeCiphertext; use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; - use spl_token_2022_interface::{ - extension::{ - BaseStateWithExtensionsMut, StateWithExtensionsMut, - confidential_transfer::{ - ConfidentialTransferAccount, instruction as confidential_instruction, - }, - }, - solana_zk_sdk::encryption::{ - auth_encryption::AeKey, - elgamal::{ElGamalKeypair, ElGamalPubkey}, - pod::auth_encryption::PodAeCiphertext, + use spl_token_2022_interface::extension::{ + BaseStateWithExtensionsMut, StateWithExtensionsMut, + confidential_transfer::{ + ConfidentialTransferAccount, instruction as confidential_instruction, }, }; use surfpool_types::types::{ @@ -10826,16 +10825,11 @@ async fn test_confidential_balance_deposit_round_trip(test_type: TestType) { &token_program, ); - // Leg 1: derive the owner's confidential keys from signatures scoped to this - // token account, which are the two messages a confidential client signs. - let (elgamal_signature, ae_signature) = - crate::types::confidential_key_signatures(&owner, &token_account); + // Leg 1: derive the owner's confidential keys from a signature scoped to + // this token account, using the SDK's canonical derivation message. + let signature = crate::types::confidential_key_signature(&owner, &token_account); let keys = rpc_server - .derive_confidential_keys( - Some(runloop_context.clone()), - elgamal_signature, - ae_signature, - ) + .derive_confidential_keys(Some(runloop_context.clone()), signature) .expect("deriveConfidentialKeys should succeed") .value; @@ -11126,6 +11120,11 @@ async fn test_confidential_balance_deposit_round_trip(test_type: TestType) { ); } +#[test_case(TestType::sqlite(); "with on-disk sqlite db")] +#[test_case(TestType::in_memory(); "with in-memory sqlite db")] +#[test_case(TestType::no_db(); "with no db")] +#[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))] +#[tokio::test(flavor = "multi_thread")] async fn test_duplicate_transaction_rejected(test_type: TestType) { let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm(); let svm_locker = SurfnetSvmLocker::new(svm_instance); @@ -11870,3 +11869,485 @@ async fn test_request_airdrop_rejects_below_rent_amount() { assert_eq!(err.code, jsonrpc_core::ErrorCode::InvalidParams); assert!(err.message.contains("rent-exempt minimum")); } + +fn build_v1_transaction( + payer: &Keypair, + instructions: &[Instruction], + recent_blockhash: Hash, + config: TransactionConfig, +) -> VersionedTransaction { + let message = v1::Message::try_compile_with_config( + &payer.pubkey(), + instructions, + recent_blockhash, + config, + ) + .expect("v1 message should compile"); + VersionedTransaction::try_new(VersionedMessage::V1(message), &[payer]) + .expect("v1 transaction should sign") +} + +async fn process_v1_transaction_and_get_fee_and_cus( + svm_locker: &SurfnetSvmLocker, + transaction: VersionedTransaction, +) -> (u64, u64) { + let signature = transaction.signatures[0]; + let (status_tx, status_rx) = crossbeam_unbounded(); + svm_locker + .process_transaction(&None, transaction, status_tx, false, true) + .await + .expect("v1 transaction processing should complete"); + + match status_rx.recv_timeout(Duration::from_secs(5)) { + Ok(TransactionStatusEvent::Success(_)) => {} + other => panic!("expected v1 transaction success, got {other:?}"), + } + + svm_locker.with_svm_reader(|svm| { + let tx_status = svm + .transactions + .get(&signature.to_string()) + .expect("transaction lookup should not fail") + .expect("processed transaction should be stored"); + let (tx_with_status_meta, _) = tx_status.expect_processed(); + ( + tx_with_status_meta.meta.fee, + tx_with_status_meta + .meta + .compute_units_consumed + .unwrap_or_default(), + ) + }) +} + +async fn process_v1_transaction_and_get_fee( + svm_locker: &SurfnetSvmLocker, + transaction: VersionedTransaction, +) -> u64 { + process_v1_transaction_and_get_fee_and_cus(svm_locker, transaction) + .await + .0 +} + +async fn process_v1_transaction_and_get_rejection_message( + svm_locker: &SurfnetSvmLocker, + transaction: VersionedTransaction, + sigverify: bool, +) -> String { + let (status_tx, status_rx) = crossbeam_unbounded(); + let result = svm_locker + .process_transaction(&None, transaction, status_tx, false, sigverify) + .await; + + match status_rx.recv_timeout(Duration::from_secs(5)) { + Ok(TransactionStatusEvent::SimulationFailure((error, _))) + | Ok(TransactionStatusEvent::ExecutionFailure((error, _))) => { + assert!( + result.is_ok(), + "runtime transaction failures should be reported on the status channel" + ); + error.to_string() + } + Ok(TransactionStatusEvent::VerificationFailure(error)) => { + assert!( + result.is_err(), + "verification failures should also be returned from process_transaction" + ); + error + } + other => panic!("expected v1 transaction rejection, got {other:?}"), + } +} + +fn funded_v1_test_svm(payer: &Keypair) -> SurfnetSvmLocker { + let (mut svm_instance, _simnet_events_rx, _geyser_events_rx) = + TestType::no_db().initialize_svm(); + svm_instance + .airdrop(&payer.pubkey(), LAMPORTS_PER_SOL) + .expect("airdrop should not fail") + .expect("airdrop should fund payer"); + SurfnetSvmLocker::new(svm_instance) +} + +/// [TransactionConfig] that will work with a transfer ix with a default system account +fn default_transaction_config() -> TransactionConfig { + TransactionConfig::empty() + // transfer ix consumes 150 CUs + .with_compute_unit_limit(150) + .with_loaded_accounts_data_size_limit(149) +} + +/// Fetch account and assert that it does not exist. Panics if the account exists. +fn assert_account_does_not_exist(svm_locker: &SurfnetSvmLocker, pubkey: &Pubkey, fail_msg: &str) { + let account = svm_locker + .with_svm_reader(|svm| svm.get_account(pubkey)) + .expect("account lookup should not fail"); + assert!(account.is_none(), "{}", fail_msg); +} + +/// Fetch account and assert that it has the expected lamports. Panics if the account does not exist. +fn assert_account_lamports( + svm_locker: &SurfnetSvmLocker, + pubkey: &Pubkey, + expected_lamports: u64, + fail_msg: &str, +) { + let account = svm_locker + .with_svm_reader(|svm| svm.get_account(pubkey)) + .expect("account lookup should not fail") + .expect("account should exist"); + assert_eq!(account.lamports, expected_lamports, "{}", fail_msg); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_v1_tx_sigverify_accepts_valid_signature() { + let payer = Keypair::new(); + let recipient = Pubkey::new_unique(); + let svm_locker = funded_v1_test_svm(&payer); + let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let transaction = build_v1_transaction( + &payer, + &[transfer(&payer.pubkey(), &recipient, 1_000_000)], + recent_blockhash, + default_transaction_config(), + ); + + process_v1_transaction_and_get_fee(&svm_locker, transaction).await; + + assert_account_lamports( + &svm_locker, + &recipient, + 1_000_000, + "valid v1 transaction should execute transfer", + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_v1_tx_sigverify_rejects_signed_config_mutation() { + let payer = Keypair::new(); + let recipient = Pubkey::new_unique(); + let svm_locker = funded_v1_test_svm(&payer); + let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let mut transaction = build_v1_transaction( + &payer, + &[transfer(&payer.pubkey(), &recipient, 1_000_000)], + recent_blockhash, + default_transaction_config().with_priority_fee(42_000), + ); + let VersionedMessage::V1(message) = &mut transaction.message else { + panic!("expected v1 transaction"); + }; + message.config = message.config.with_priority_fee(42_001); + + let error = + process_v1_transaction_and_get_rejection_message(&svm_locker, transaction, true).await; + assert!( + error.contains("Transaction did not pass signature verification"), + "mutating a signed v1 config field should fail signature verification, got: {error}" + ); + + assert_account_does_not_exist( + &svm_locker, + &recipient, + "signature verification failure should not execute the transfer", + ) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_v1_tx_send_transaction_respects_sigverify_flag() { + let payer = Keypair::new(); + let recipient = Pubkey::new_unique(); + let svm_locker = funded_v1_test_svm(&payer); + let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let mut invalid_signature_tx = build_v1_transaction( + &payer, + &[transfer(&payer.pubkey(), &recipient, 1_000_000)], + recent_blockhash, + default_transaction_config(), + ); + invalid_signature_tx.signatures[0] = solana_signature::Signature::new_unique(); + + let error = process_v1_transaction_and_get_rejection_message( + &svm_locker, + invalid_signature_tx.clone(), + true, + ) + .await; + assert!( + error.contains("Transaction did not pass signature verification"), + "v1 transaction with invalid signature should be rejected, got: {error}" + ); + + let (status_tx, status_rx) = crossbeam_unbounded(); + svm_locker + .process_transaction(&None, invalid_signature_tx, status_tx, false, false) + .await + .expect("skip signature verification should allow the v1 transaction to process"); + match status_rx.recv_timeout(Duration::from_secs(5)) { + Ok(TransactionStatusEvent::Success(_)) => {} + other => panic!("expected v1 transaction success with sigverify disabled, got {other:?}"), + } + + assert_account_lamports( + &svm_locker, + &recipient, + 1_000_000, + "v1 transaction with sigverify disabled should execute transfer", + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_v1_tx_priority_fee_config_is_charged() { + let payer_without_priority_fee = Keypair::new(); + let payer_with_priority_fee = Keypair::new(); + let recipient = Pubkey::new_unique(); + let priority_fee = 42_000; + + let no_priority_fee_svm = funded_v1_test_svm(&payer_without_priority_fee); + let no_priority_fee_blockhash = + no_priority_fee_svm.with_svm_reader(|svm| svm.latest_blockhash()); + let no_priority_fee_tx = build_v1_transaction( + &payer_without_priority_fee, + &[transfer( + &payer_without_priority_fee.pubkey(), + &recipient, + 1_000_000, + )], + no_priority_fee_blockhash, + default_transaction_config(), + ); + let no_priority_fee = + process_v1_transaction_and_get_fee(&no_priority_fee_svm, no_priority_fee_tx).await; + + let priority_fee_svm = funded_v1_test_svm(&payer_with_priority_fee); + let priority_fee_blockhash = priority_fee_svm.with_svm_reader(|svm| svm.latest_blockhash()); + let priority_fee_tx = build_v1_transaction( + &payer_with_priority_fee, + &[transfer( + &payer_with_priority_fee.pubkey(), + &recipient, + 1_000_000, + )], + priority_fee_blockhash, + default_transaction_config().with_priority_fee(priority_fee), + ); + let fee_with_priority_fee = + process_v1_transaction_and_get_fee(&priority_fee_svm, priority_fee_tx).await; + + assert_eq!(fee_with_priority_fee - no_priority_fee, priority_fee); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_v1_tx_heap_size_config_bounds_are_rejected_by_surfpool() { + const MIN_HEAP_FRAME_BYTES: u32 = 32 * 1024; + const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024; + const TRANSFER_LAMPORTS: u64 = 1_000_000; + + let payer = Keypair::new(); + let svm_locker = funded_v1_test_svm(&payer); + + for heap_size in [MIN_HEAP_FRAME_BYTES, MAX_HEAP_FRAME_BYTES] { + let recipient = Pubkey::new_unique(); + let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let transaction = build_v1_transaction( + &payer, + &[transfer(&payer.pubkey(), &recipient, TRANSFER_LAMPORTS)], + recent_blockhash, + default_transaction_config().with_heap_size(heap_size), + ); + process_v1_transaction_and_get_fee(&svm_locker, transaction).await; + + assert_account_lamports( + &svm_locker, + &recipient, + TRANSFER_LAMPORTS, + "v1 transaction with valid heap size should execute transfer", + ); + } + + for heap_size in [ + MIN_HEAP_FRAME_BYTES - 1, + MIN_HEAP_FRAME_BYTES + 1, + MAX_HEAP_FRAME_BYTES + 1, + ] { + let recipient = Pubkey::new_unique(); + let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let transaction = build_v1_transaction( + &payer, + &[transfer(&payer.pubkey(), &recipient, TRANSFER_LAMPORTS)], + recent_blockhash, + default_transaction_config().with_heap_size(heap_size), + ); + let error = + process_v1_transaction_and_get_rejection_message(&svm_locker, transaction, true).await; + assert!( + error.to_ascii_lowercase().contains("sanitize") + || error.to_ascii_lowercase().contains("invalid"), + "invalid v1 heap size {heap_size} should be rejected by Surfpool, got: {error}" + ); + + assert_account_does_not_exist( + &svm_locker, + &recipient, + &format!("invalid heap size {heap_size} should not execute the transfer"), + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_v1_tx_ignores_invalid_compute_budget_instruction() { + let payer = Keypair::new(); + let recipient = Pubkey::new_unique(); + let svm_locker = funded_v1_test_svm(&payer); + let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let baseline_transaction = build_v1_transaction( + &payer, + &[transfer(&payer.pubkey(), &recipient, 1_000_000)], + recent_blockhash, + default_transaction_config(), + ); + let (_, baseline_cus) = + process_v1_transaction_and_get_fee_and_cus(&svm_locker, baseline_transaction).await; + + let second_recipient = Pubkey::new_unique(); + let second_recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let invalid_compute_budget_ix = Instruction { + program_id: solana_compute_budget_interface::id(), + accounts: vec![], + data: vec![u8::MAX], + }; + let transaction = build_v1_transaction( + &payer, + &[ + invalid_compute_budget_ix, + transfer(&payer.pubkey(), &second_recipient, 1_000_000), + ], + second_recent_blockhash, + // Extra and compute limit account data size is needed to load compute budget account + default_transaction_config() + .with_loaded_accounts_data_size_limit(298) + .with_compute_unit_limit(3_000), + ); + + let (_, cus_with_noop_compute_budget_ix) = + process_v1_transaction_and_get_fee_and_cus(&svm_locker, transaction).await; + assert!( + cus_with_noop_compute_budget_ix > baseline_cus, + "v1 compute-budget instruction should consume compute units as a successful no-op" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_v1_tx_ignores_duplicate_compute_budget_configuration_instructions() { + let payer = Keypair::new(); + let recipient = Pubkey::new_unique(); + let svm_locker = funded_v1_test_svm(&payer); + let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let baseline_transaction = build_v1_transaction( + &payer, + &[transfer(&payer.pubkey(), &recipient, 1_000_000)], + recent_blockhash, + default_transaction_config(), + ); + let (baseline_fee, baseline_cus) = + process_v1_transaction_and_get_fee_and_cus(&svm_locker, baseline_transaction).await; + + let second_recipient = Pubkey::new_unique(); + let second_recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let transaction = build_v1_transaction( + &payer, + &[ + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + ComputeBudgetInstruction::set_compute_unit_limit(1_399_999), + ComputeBudgetInstruction::set_compute_unit_price(1_000), + ComputeBudgetInstruction::set_compute_unit_price(2_000), + transfer(&payer.pubkey(), &second_recipient, 1_000_000), + ], + second_recent_blockhash, + // Extra compute limit and account data size is needed to load compute budget account + default_transaction_config() + .with_loaded_accounts_data_size_limit(298) + .with_compute_unit_limit(3_000), + ); + + let (fee_with_ignored_compute_budget_ixs, cus_with_noop_compute_budget_ixs) = + process_v1_transaction_and_get_fee_and_cus(&svm_locker, transaction).await; + + assert_eq!( + fee_with_ignored_compute_budget_ixs, baseline_fee, + "v1 compute-budget price instructions should not configure prioritization fees" + ); + assert!( + cus_with_noop_compute_budget_ixs > baseline_cus, + "v1 compute-budget instructions should consume compute units as successful no-ops" + ); + + assert_account_lamports( + &svm_locker, + &second_recipient, + 1_000_000, + "v1 transaction with duplicate compute-budget instructions should execute transfer", + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_v1_tx_insufficient_loaded_account_data_size_limit_rejects_transaction() { + let payer = Keypair::new(); + let recipient = Pubkey::new_unique(); + let svm_locker = funded_v1_test_svm(&payer); + let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let transaction = build_v1_transaction( + &payer, + &[transfer(&payer.pubkey(), &recipient, 1_000_000)], + recent_blockhash, + TransactionConfig::empty() + // sufficient compute unit limit + .with_compute_unit_limit(150) + // insufficient loaded accounts data size limit + .with_loaded_accounts_data_size_limit(148), + ); + + let error = + process_v1_transaction_and_get_rejection_message(&svm_locker, transaction, true).await; + assert_eq!( + error, + "Transaction exceeded max loaded accounts data size cap" + ); + + assert_account_does_not_exist( + &svm_locker, + &recipient, + "invalid v1 transaction should not execute the transfer", + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_v1_tx_insufficient_compute_unit_limit_rejects_transaction() { + let payer = Keypair::new(); + let recipient = Pubkey::new_unique(); + let svm_locker = funded_v1_test_svm(&payer); + let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash()); + let transaction = build_v1_transaction( + &payer, + &[transfer(&payer.pubkey(), &recipient, 1_000_000)], + recent_blockhash, + TransactionConfig::empty() + // insufficient compute unit limit + .with_compute_unit_limit(149) + // sufficient loaded accounts data size limit + .with_loaded_accounts_data_size_limit(149), + ); + + let error = + process_v1_transaction_and_get_rejection_message(&svm_locker, transaction, true).await; + assert_eq!( + error, + "Error processing Instruction 0: Computational budget exceeded" + ); + + assert_account_does_not_exist( + &svm_locker, + &recipient, + "invalid v1 transaction should not execute the transfer", + ); +} diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 48d560020..15f00fbfb 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -39,24 +39,23 @@ use solana_transaction_status::{ }, parse_ui_inner_instructions, }; -use spl_token_2022_interface::{ - extension::{ - BaseStateWithExtensions, BaseStateWithExtensionsMut, ExtensionType, StateWithExtensions, - StateWithExtensionsMut, - confidential_transfer::{ConfidentialTransferAccount, PENDING_BALANCE_LO_BIT_LENGTH}, - confidential_transfer_fee::ConfidentialTransferFeeAmount, - interest_bearing_mint::InterestBearingConfig, - scaled_ui_amount::ScaledUiAmountConfig, - transfer_fee::TransferFeeConfig, - }, - solana_zk_sdk::encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey}, - pod::{ - auth_encryption::PodAeCiphertext, - elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - }, - }, +use solana_zk_sdk::encryption::{ + auth_encryption::{AeCiphertext, AeKey}, + derivation::derive_confidential_keys_from_signature, + elgamal::{ElGamalCiphertext, ElGamalPubkey, ElGamalSecretKey}, +}; +use solana_zk_sdk_pod::encryption::{ + auth_encryption::PodAeCiphertext, + elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, +}; +use spl_token_2022_interface::extension::{ + BaseStateWithExtensions, BaseStateWithExtensionsMut, ExtensionType, StateWithExtensions, + StateWithExtensionsMut, + confidential_transfer::{ConfidentialTransferAccount, PENDING_BALANCE_LO_BIT_LENGTH}, + confidential_transfer_fee::ConfidentialTransferFeeAmount, + interest_bearing_mint::InterestBearingConfig, + scaled_ui_amount::ScaledUiAmountConfig, + transfer_fee::TransferFeeConfig, }; use surfpool_types::types::{ ConfidentialBalanceKeys, ConfidentialTransferAccountUpdate, DeriveConfidentialKeysResponse, @@ -1518,30 +1517,22 @@ fn decrypt_pending_balance( }) } -/// Derive an owner's confidential-transfer keys from the owner's signatures. +/// Derive an owner's confidential-transfer keys from the owner's signature. /// -/// Backs the `surfnet_deriveConfidentialKeys` cheatcode: the caller signs the two seed -/// messages and gets back keys ready to pass to the other confidential cheatcodes. +/// Backs the `surfnet_deriveConfidentialKeys` cheatcode: the caller signs the +/// canonical confidential-balance derivation message and gets back keys ready to +/// pass to the other confidential cheatcodes. /// -/// The derivation semantics — domain separation, the `new_from_signer` equivalence, and -/// what does and does not cross the wire — are documented on `surfnet_deriveConfidentialKeys` -/// in [`crate::rpc::surfnet_cheatcodes::SurfnetCheatcodes`]. +/// The SDK's domain separation and what does and does not cross the wire are documented on +/// `surfnet_deriveConfidentialKeys` in [`crate::rpc::surfnet_cheatcodes::SurfnetCheatcodes`]. /// /// `derive_confidential_keys` itself imposes no seed: the caller owns what the keys are /// scoped to, because the caller owns what it signed. -pub fn derive_confidential_keys( - elgamal_signature: &str, - ae_signature: &str, -) -> Result { - let elgamal_signature = parse_confidential_key_signature(elgamal_signature) - .map_err(|e| format!("elgamalSignature: {e}"))?; - let ae_signature = - parse_confidential_key_signature(ae_signature).map_err(|e| format!("aeSignature: {e}"))?; - - let elgamal = ElGamalKeypair::new_from_signature(&elgamal_signature) - .map_err(|e| format!("failed to derive ElGamal keypair: {e}"))?; - let aes_key = AeKey::new_from_signature(&ae_signature) - .map_err(|e| format!("failed to derive AES key: {e}"))?; +pub fn derive_confidential_keys(signature: &str) -> Result { + let signature = + parse_confidential_key_signature(signature).map_err(|e| format!("signature: {e}"))?; + let (elgamal, aes_key) = derive_confidential_keys_from_signature(&signature) + .map_err(|e| format!("failed to derive confidential keys: {e}"))?; let elgamal_secret_key: [u8; 32] = elgamal.secret().into(); let aes_key: [u8; 16] = aes_key.into(); @@ -1553,24 +1544,25 @@ pub fn derive_confidential_keys( }) } -/// Sign the two seed messages `derive_confidential_keys` expects, scoping the keys +/// Sign the canonical confidential-balance derivation message, scoping the keys /// to `token_account`. /// -/// This is the client-side half of the cheatcode: it mirrors, in the open, what -/// `ElGamalKeypair::new_from_signer` and `AeKey::new_from_signer` sign internally. +/// This is the client-side half of the cheatcode and mirrors +/// `solana_zk_sdk::encryption::derivation::derive_confidential_keys`. #[cfg(test)] -pub(crate) fn confidential_key_signatures( +pub(crate) fn confidential_key_signature( owner: &solana_keypair::Keypair, token_account: &Pubkey, -) -> (String, String) { +) -> String { use solana_signer::Signer; - let sign = |domain: &[u8]| { - owner - .sign_message(&[domain, token_account.as_ref()].concat()) - .to_string() - }; - (sign(b"ElGamalSecretKey"), sign(b"AeKey")) + owner + .sign_message( + &solana_zk_sdk::encryption::derivation::confidential_derivation_message( + token_account.as_ref(), + ), + ) + .to_string() } #[cfg(test)] @@ -1579,79 +1571,69 @@ mod confidential_key_derivation_tests { use super::*; - /// The whole point of moving from a keypair to signatures is that the keys do - /// not change. Derive both ways over the same owner and token account and - /// compare: `new_from_signer` signs `"ElGamalSecretKey" || seed` and - /// `"AeKey" || seed` itself and then calls the very `new_from_signature` - /// functions the cheatcode now calls, so the two paths must agree byte for byte. + /// The canonical signer and precomputed-signature paths must produce the same + /// HKDF-derived key material. #[test] fn signatures_reproduce_the_keys_the_signer_path_derived() { let owner = Keypair::new(); let token_account = Pubkey::new_unique(); - let (elgamal_signature, ae_signature) = confidential_key_signatures(&owner, &token_account); - let from_signatures = derive_confidential_keys(&elgamal_signature, &ae_signature) - .expect("deriving from signatures should succeed"); + let signature = confidential_key_signature(&owner, &token_account); + let from_signature = + derive_confidential_keys(&signature).expect("deriving from signatures should succeed"); - // The reference path, with the standard per-account seed and no prefix. - let seed = token_account.as_ref(); - let elgamal = ElGamalKeypair::new_from_signer(&owner, seed).unwrap(); - let aes_key = AeKey::new_from_signer(&owner, seed).unwrap(); + let (elgamal, aes_key) = solana_zk_sdk::encryption::derivation::derive_confidential_keys( + &owner, + token_account.as_ref(), + ) + .unwrap(); let elgamal_secret: [u8; 32] = elgamal.secret().into(); let aes_key_bytes: [u8; 16] = aes_key.into(); assert_eq!( - from_signatures.elgamal_pubkey, + from_signature.elgamal_pubkey, bs58::encode(bytes_of(&PodElGamalPubkey::from(elgamal.pubkey_owned()))).into_string(), ); assert_eq!( - from_signatures.elgamal_secret_key, + from_signature.elgamal_secret_key, bs58::encode(elgamal_secret).into_string(), ); assert_eq!( - from_signatures.aes_key, + from_signature.aes_key, bs58::encode(aes_key_bytes).into_string(), ); } - /// The two seed messages are domain-separated, so reusing one signature for - /// both keys does not reproduce the signer path. This is why the cheatcode - /// takes two signatures rather than one. + /// The account seed is part of the canonical signed message, so changing it + /// changes both keys. #[test] - fn one_signature_cannot_stand_in_for_both() { + fn different_seed_produces_different_keys() { let owner = Keypair::new(); let token_account = Pubkey::new_unique(); - let (elgamal_signature, ae_signature) = confidential_key_signatures(&owner, &token_account); - assert_ne!(elgamal_signature, ae_signature); - - let reused = derive_confidential_keys(&elgamal_signature, &elgamal_signature).unwrap(); - let correct = derive_confidential_keys(&elgamal_signature, &ae_signature).unwrap(); + let current = + derive_confidential_keys(&confidential_key_signature(&owner, &token_account)).unwrap(); + let other = + derive_confidential_keys(&confidential_key_signature(&owner, &Pubkey::new_unique())) + .unwrap(); - assert_eq!(reused.elgamal_pubkey, correct.elgamal_pubkey); - assert_ne!(reused.aes_key, correct.aes_key); + assert_ne!(current.elgamal_pubkey, other.elgamal_pubkey); + assert_ne!(current.aes_key, other.aes_key); } - /// `new_from_signature` hashes the all-zero default signature happily, so - /// without an explicit check the cheatcode would hand back a fixed key pair - /// anyone can compute. `new_from_signer`, the path this replaced, rejects it. - /// Assert the rejection on both parameters and that the error names which one. + /// The SDK rejects the all-zero signature so the cheatcode cannot hand back a + /// fixed, publicly computable key pair. #[test] - fn the_default_signature_is_rejected_on_both_parameters() { + fn the_default_signature_is_rejected() { let owner = Keypair::new(); let token_account = Pubkey::new_unique(); - let (elgamal_signature, ae_signature) = confidential_key_signatures(&owner, &token_account); + let signature = confidential_key_signature(&owner, &token_account); let default_signature = Signature::default().to_string(); - // Why the check has to live here: the SDK functions this calls accept it. - assert!(ElGamalKeypair::new_from_signature(&Signature::default()).is_ok()); - assert!(AeKey::new_from_signature(&Signature::default()).is_ok()); - - let error = derive_confidential_keys(&default_signature, &ae_signature).unwrap_err(); - assert!(error.starts_with("elgamalSignature:"), "got: {error}"); + let error = derive_confidential_keys(&default_signature).unwrap_err(); + assert!(error.starts_with("signature:"), "got: {error}"); - let error = derive_confidential_keys(&elgamal_signature, &default_signature).unwrap_err(); - assert!(error.starts_with("aeSignature:"), "got: {error}"); + assert!(derive_confidential_keys(&signature).is_ok()); } /// A 32-byte pubkey is valid base58 but is not a signature, and the error has @@ -1660,14 +1642,13 @@ mod confidential_key_derivation_tests { fn a_malformed_signature_names_its_parameter() { let owner = Keypair::new(); let token_account = Pubkey::new_unique(); - let (elgamal_signature, ae_signature) = confidential_key_signatures(&owner, &token_account); + let signature = confidential_key_signature(&owner, &token_account); let not_a_signature = token_account.to_string(); - let error = derive_confidential_keys(¬_a_signature, &ae_signature).unwrap_err(); - assert!(error.starts_with("elgamalSignature:"), "got: {error}"); + let error = derive_confidential_keys(¬_a_signature).unwrap_err(); + assert!(error.starts_with("signature:"), "got: {error}"); - let error = derive_confidential_keys(&elgamal_signature, ¬_a_signature).unwrap_err(); - assert!(error.starts_with("aeSignature:"), "got: {error}"); + assert!(derive_confidential_keys(&signature).is_ok()); } } diff --git a/crates/sdk-node/surfpool-sdk/kit/types/api.ts b/crates/sdk-node/surfpool-sdk/kit/types/api.ts index 339f39b2d..0c8de0433 100644 --- a/crates/sdk-node/surfpool-sdk/kit/types/api.ts +++ b/crates/sdk-node/surfpool-sdk/kit/types/api.ts @@ -118,7 +118,7 @@ export type SurfnetGetConfidentialBalanceApi = { getConfidentialBalance(tokenAccount: Address, keys: ConfidentialBalanceKeys): GetConfidentialBalanceResponse; }; export type SurfnetDeriveConfidentialKeysApi = { - deriveConfidentialKeys(elgamalSignature: string, aeSignature: string): DeriveConfidentialKeysResponse; + deriveConfidentialKeys(signature: string): DeriveConfidentialKeysResponse; }; export type SurfnetResetAccountApi = { resetAccount(pubkey: Address, config?: ResetAccountConfig): null; From 21996010a9e26f18446f50a7c8238f0dee6711e7 Mon Sep 17 00:00:00 2001 From: Thektonic <106833310+Thektonic@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:29:01 +0200 Subject: [PATCH 07/13] fix(tests): convert RpcClient to the expected type in the genesis-hash locker test (#793) --- crates/core/src/surfnet/locker.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/core/src/surfnet/locker.rs b/crates/core/src/surfnet/locker.rs index 443501b5c..fcdb7dd78 100644 --- a/crates/core/src/surfnet/locker.rs +++ b/crates/core/src/surfnet/locker.rs @@ -4716,7 +4716,8 @@ mod tests { requests: Arc::clone(&requests), }, RpcClientConfig::default(), - ), + ) + .into(), }; let remote_ctx = Some(remote_client); From 7c9d214cba9565d9504cf2316cd8ecc5d2d75f90 Mon Sep 17 00:00:00 2001 From: Georgi Petroff <102983346+92Infinitus92@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:25:33 +0300 Subject: [PATCH 08/13] feat(mcp): split template discovery into index and detail tools (#768) --- crates/mcp/src/surfpool/mod.rs | 168 ++++++++++++++++++++++++++++++--- 1 file changed, 156 insertions(+), 12 deletions(-) diff --git a/crates/mcp/src/surfpool/mod.rs b/crates/mcp/src/surfpool/mod.rs index 7ce4c3e08..d98665cbd 100644 --- a/crates/mcp/src/surfpool/mod.rs +++ b/crates/mcp/src/surfpool/mod.rs @@ -100,6 +100,18 @@ fn compact_template_json(template: &surfpool_types::OverrideTemplate) -> serde_j obj } +fn index_template_json(template: &surfpool_types::OverrideTemplate) -> serde_json::Value { + serde_json::json!({ + "id": template.id, + "name": template.name, + "description": template.description, + "protocol": template.protocol, + "accountType": template.account_type, + "tags": template.tags, + "hasLlmContext": template.llm_context.is_some(), + }) +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct SearchConstantOptionsParams { @@ -130,6 +142,15 @@ pub struct CreatePumpGraduationScenarioParams { pub surfnet_port: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct GetTemplateParams { + #[schemars( + description = "Template id from get_override_templates (e.g., \"pyth-price-feed-v2\")." + )] + pub template_id: String, +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct StartSurfnetWithTokenAccountsParams { #[schemars( @@ -644,7 +665,7 @@ impl Surfpool { IMPORTANT: - If a user asks to create a scenario, DO NOT USE this method. Instead, use the dedicated `create_scenario` tool. - - There is NO RPC method called `surfnet_getOverrideTemplates`. Override templates are ONLY available via the MCP resource str:///override_templates (use read_resource, not this RPC tool). + - There is NO RPC method called `surfnet_getOverrideTemplates`. The override templates index is available via the MCP resource str:///override_templates (use read_resource, not this RPC tool); one template's full detail comes from the get_override_template tool. "#)] async fn call_surfnet_rpc( &self, @@ -729,7 +750,8 @@ impl Surfpool { - The `tags` field MUST be a JSON array [], NOT a JSON string - DO NOT stringify nested objects - pass them as native JSON - ⚠️ CRITICAL: You MUST call `get_override_templates` FIRST to get valid template data. + ⚠️ CRITICAL: You MUST call `get_override_templates` FIRST to pick a templateId, then + `get_override_template` on that id for its properties and account shape. DO NOT invent templateId values, property names, or account addresses - they MUST come from the templates. STRICT RULES - VIOLATIONS WILL CAUSE ERRORS: @@ -1002,7 +1024,7 @@ impl Surfpool { } #[tool( - description = "Fetches ALL available override templates. MUST be called before create_scenario to get valid templateId values and property names. Constants are summarized as {label, description, optionsCount} - resolve an actual option value with search_constant_options." + 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." )] async fn get_override_templates(&self) -> Result { let registry = self.template_registry.read().map_err(|_| { @@ -1017,13 +1039,42 @@ impl Surfpool { let templates: Vec = registry .all() .iter() - .map(|t| compact_template_json(t)) + .map(|t| index_template_json(t)) .collect(); let json_str = serde_json::to_string(&templates).unwrap_or_default(); Ok(CallToolResult::success(vec![Content::text(json_str)])) } + #[tool( + description = "Fetches one template's full detail (properties, address, constants summarized as {label, description, optionsCount}, and llmContext). Call after get_override_templates with the id you picked, before create_scenario. Resolve an actual constant option value with search_constant_options." + )] + async fn get_override_template( + &self, + Parameters(params): Parameters, + ) -> Result { + let registry = self.template_registry.read().map_err(|_| { + use std::borrow::Cow; + McpError { + code: ErrorCode(-32603), + message: Cow::from("Failed to read template registry"), + data: None, + } + })?; + + let all_templates = registry.all(); + let Some(template) = all_templates.iter().find(|t| t.id == params.template_id) else { + let valid_ids: Vec<&String> = all_templates.iter().map(|t| &t.id).collect(); + return Ok(CallToolResult::error(vec![Content::text(format!( + "Unknown templateId {:?}. Valid IDs are: {:?}", + params.template_id, valid_ids + ))])); + }; + + let json_str = serde_json::to_string(&compact_template_json(template)).unwrap_or_default(); + Ok(CallToolResult::success(vec![Content::text(json_str)])) + } + #[tool( description = "Searches the options of a template's constants (price feeds, markets, token mints). Use after get_override_templates to resolve a constant_ref value: pass the templateId, optionally the constant name, and a query like \"SOL/USD\". Returns matching options whose `value` field is what create_scenario expects." )] @@ -1208,7 +1259,7 @@ impl ServerHandler for Surfpool { let templates: Vec = registry .all() .iter() - .map(|t| compact_template_json(t)) + .map(|t| index_template_json(t)) .collect(); let templates_json = serde_json::to_string(&templates).map_err(|_| { @@ -1315,8 +1366,14 @@ mod tests { }) } + fn template_id(id: &str) -> Parameters { + Parameters(GetTemplateParams { + template_id: id.to_string(), + }) + } + #[tokio::test] - async fn get_override_templates_summarizes_constants_instead_of_inlining_options() { + async fn the_template_index_is_light_and_omits_the_heavy_fields() { let surfpool = Surfpool::new(); let result = surfpool.get_override_templates().await.unwrap(); assert_ne!(result.is_error, Some(true)); @@ -1329,6 +1386,38 @@ mod tests { .find(|t| t["id"] == "pyth-price-feed-v2") .expect("pyth template present"); + for heavy in ["llmContext", "properties", "address", "constants", "idl"] { + assert!( + pyth.get(heavy).is_none(), + "the index must not carry {heavy}" + ); + } + assert_eq!( + pyth["hasLlmContext"], true, + "the index flags which templates have context to fetch" + ); + } + + #[tokio::test] + async fn get_override_template_returns_full_detail_and_summarizes_constants() { + let surfpool = Surfpool::new(); + let result = surfpool + .get_override_template(template_id("pyth-price-feed-v2")) + .await + .unwrap(); + assert_ne!(result.is_error, Some(true)); + + let pyth = json_of(&result); + assert!( + pyth.get("properties").is_some(), + "detail carries properties" + ); + assert!(pyth.get("address").is_some(), "detail carries the address"); + assert!( + pyth.get("llmContext").is_some(), + "detail carries llmContext" + ); + let price_feed = &pyth["constants"]["price_feed"]; assert!( price_feed["optionsCount"].as_u64().unwrap() > 0, @@ -1340,6 +1429,44 @@ mod tests { ); } + #[tokio::test] + async fn get_override_template_rejects_an_unknown_id_and_names_valid_ones() { + let surfpool = Surfpool::new(); + let result = surfpool + .get_override_template(template_id("no-such-template")) + .await + .unwrap(); + assert_eq!(result.is_error, Some(true)); + let text = &result.content[0].as_text().expect("text").text; + assert!( + text.contains("pyth-price-feed-v2"), + "the error must name valid ids, got: {text}" + ); + } + + #[test] + fn the_index_is_lighter_than_the_full_detail_of_every_template() { + let registry = TemplateRegistry::new(); + let index: Vec<_> = registry + .all() + .iter() + .map(|t| index_template_json(t)) + .collect(); + let full: Vec<_> = registry + .all() + .iter() + .map(|t| compact_template_json(t)) + .collect(); + + let index_len = serde_json::to_string(&index).unwrap().len(); + let full_len = serde_json::to_string(&full).unwrap().len(); + assert!( + index_len * 3 < full_len, + "the index ({index_len} bytes) should be far lighter than the old full payload \ + ({full_len} bytes)" + ); + } + #[tokio::test] async fn search_finds_a_feed_case_insensitively_and_returns_usable_values() { let surfpool = Surfpool::new(); @@ -1402,6 +1529,22 @@ mod tests { } } + #[test] + fn index_template_json_omits_the_heavy_fields() { + let registry = TemplateRegistry::new(); + for template in registry.all() { + let json = index_template_json(template); + for heavy in ["llmContext", "properties", "address", "constants", "idl"] { + assert!( + json.get(heavy).is_none(), + "index entry for {} leaks {heavy}", + template.id + ); + } + assert_eq!(json["hasLlmContext"], template.llm_context.is_some()); + } + } + #[tokio::test] async fn search_rejects_unknown_template_and_unknown_constant() { let surfpool = Surfpool::new(); @@ -1420,7 +1563,7 @@ mod tests { } #[tokio::test] - async fn get_override_templates_lists_the_pump_templates_compactly() { + async fn the_pump_templates_appear_in_the_index_and_detail_summarizes_the_catalog() { let surfpool = Surfpool::new(); let result = surfpool.get_override_templates().await.unwrap(); assert_ne!(result.is_error, Some(true)); @@ -1437,17 +1580,18 @@ mod tests { let template = templates .iter() .find(|t| t["id"] == id) - .unwrap_or_else(|| panic!("template {id} missing from the model's view")); + .unwrap_or_else(|| panic!("template {id} missing from the index")); assert!( template.get("idl").is_none(), - "{id} must not inline the ~160KB IDL into the LLM context" + "{id} must not inline the ~160KB IDL into the index" ); } - let curve = templates - .iter() - .find(|t| t["id"] == "pump-bonding-curve-custom") + let detail = surfpool + .get_override_template(template_id("pump-bonding-curve-custom")) + .await .unwrap(); + let curve = json_of(&detail); let token_mint = &curve["constants"]["token_mint"]; assert!( token_mint["optionsCount"].as_u64().unwrap() > 0, From 36e3de10c5fa6c0ca4fe8c9485dfb9bf509d15e7 Mon Sep 17 00:00:00 2001 From: cds-amal Date: Thu, 3 Sep 2026 09:53:23 -0400 Subject: [PATCH 09/13] fix(core): serialize postgres table creation with a tx-scoped advisory lock (#790) --- crates/core/src/storage/postgres.rs | 162 +++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 3 deletions(-) diff --git a/crates/core/src/storage/postgres.rs b/crates/core/src/storage/postgres.rs index 28d49a7ea..6d589e322 100644 --- a/crates/core/src/storage/postgres.rs +++ b/crates/core/src/storage/postgres.rs @@ -6,7 +6,7 @@ use std::{ use log::debug; use serde::{Deserialize, Serialize}; use surfpool_db::diesel::{ - self, RunQueryDsl, + self, Connection, RunQueryDsl, connection::SimpleConnection, r2d2::{ConnectionManager, Pool}, sql_query, @@ -135,8 +135,15 @@ where debug!("Getting connection from pool for table creation"); let mut conn = self.pool.get().map_err(|_| StorageError::LockError)?; - conn.batch_execute(&create_table_sql) - .map_err(|e| StorageError::create_table(&self.table_name, NAME, e))?; + // pg_advisory_xact_lock serializes this transaction, at the server, + // against every other session taking the same key, preventing a race. + conn.transaction(|conn| { + sql_query("SELECT pg_advisory_xact_lock(hashtext('surfpool:ddl:' || $1))") + .bind::(&self.table_name) + .execute(conn)?; + conn.batch_execute(&create_table_sql) + }) + .map_err(|e| StorageError::create_table(&self.table_name, NAME, e))?; debug!("Successfully ensured table '{}' exists", self.table_name); Ok(()) @@ -358,3 +365,152 @@ where Ok(Box::new(iter)) } } + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Barrier}; + + use surfpool_db::diesel::QueryableByName; + + use super::*; + use crate::storage::tests::{POSTGRES_TEST_URL_ENV, random_surfnet_id}; + + fn test_url() -> Option { + std::env::var(POSTGRES_TEST_URL_ENV).ok() + } + + fn random_table_name() -> String { + format!("ddl_race_{}", random_surfnet_id().replace('-', "")) + } + + fn drop_tables(url: &str, tables: &[String]) { + let pool = get_or_create_shared_pool(url).unwrap(); + let mut conn = pool.get().unwrap(); + for table in tables { + let _ = conn.batch_execute(&format!("DROP TABLE IF EXISTS {}", table)); + } + } + + /// Two sessions running CREATE TABLE IF NOT EXISTS for the same new + /// table can both pass the existence check; the loser's catalog + /// insert then fails with a duplicate key on pg_type_typname_nsp_index + /// and storage construction fails over a table that exists. Each + /// attempt uses a fresh table name so every attempt replays the + /// creation window that CI replays once per container. + #[test] + fn concurrent_open_store_survives_the_create_race() { + let Some(url) = test_url() else { + println!("skipping: {} not set", POSTGRES_TEST_URL_ENV); + return; + }; + const ATTEMPTS: usize = 50; + const SESSIONS: usize = 4; + + let mut tables = Vec::with_capacity(ATTEMPTS); + let mut lost = 0usize; + let mut first_loss = None; + for _ in 0..ATTEMPTS { + let table = random_table_name(); + tables.push(table.clone()); + let barrier = Arc::new(Barrier::new(SESSIONS)); + let handles: Vec<_> = (0..SESSIONS) + .map(|_| { + let url = url.clone(); + let table = table.clone(); + let barrier = barrier.clone(); + std::thread::spawn(move || { + let backend = PostgresBackend::open(&url, &random_surfnet_id()).unwrap(); + barrier.wait(); + backend.open_store::(&table).map(|_| ()) + }) + }) + .collect(); + let mut attempt_lost = false; + for handle in handles { + if let Err(e) = handle.join().unwrap() { + attempt_lost = true; + first_loss.get_or_insert(e); + } + } + if attempt_lost { + lost += 1; + } + } + drop_tables(&url, &tables); + + assert!( + lost == 0, + "lost the DDL race in {}/{} attempts; first loss: {:?}", + lost, + ATTEMPTS, + first_loss.unwrap() + ); + } + + #[derive(QueryableByName)] + struct LockProbe { + #[diesel(sql_type = diesel::sql_types::Bool)] + free: bool, + } + + /// True when no session holds the DDL advisory lock for this table. + /// Probes with try-lock from a fresh transaction; the probe's own + /// lock evaporates when its transaction ends. + /// + /// The probe connection is established outside the pool on purpose: + /// advisory locks are reentrant within a session, so a probe drawn + /// from the pool can land on the very connection that leaked the + /// lock and report it free. A dedicated connection is a distinct + /// session by construction, which is what the probe's question is + /// about. + fn ddl_lock_is_free(url: &str, table: &str) -> bool { + let mut conn = diesel::PgConnection::establish(url).unwrap(); + conn.transaction(|conn| { + sql_query("SELECT pg_try_advisory_xact_lock(hashtext('surfpool:ddl:' || $1)) AS free") + .bind::(table) + .get_result::(conn) + .map(|row| row.free) + }) + .unwrap() + } + + /// The lock is transaction-scoped, so a successful construction leaves + /// it free for the next session the moment its transaction commits. + #[test] + fn ddl_lock_is_released_after_successful_create() { + let Some(url) = test_url() else { + println!("skipping: {} not set", POSTGRES_TEST_URL_ENV); + return; + }; + let table = random_table_name(); + let backend = PostgresBackend::open(&url, &random_surfnet_id()).unwrap(); + backend.open_store::(&table).unwrap(); + let free = ddl_lock_is_free(&url, &table); + drop_tables(&url, std::slice::from_ref(&table)); + assert!(free, "the DDL advisory lock outlived a successful create"); + } + + /// The wedge regression: an error between lock and unlock must not + /// leak the lock into the pool, where it would park every later + /// constructor of this table forever. A table name that breaks the + /// CREATE forces the error path after the lock is taken; the database + /// releases the lock when the transaction rolls back. + #[test] + fn ddl_lock_is_released_after_failed_create() { + let Some(url) = test_url() else { + println!("skipping: {} not set", POSTGRES_TEST_URL_ENV); + return; + }; + let table = "ddl race bad name"; // spaces break the unquoted CREATE + let backend = PostgresBackend::open(&url, &random_surfnet_id()).unwrap(); + let result = backend.open_store::(table); + assert!( + result.is_err(), + "a syntactically broken CREATE should fail construction" + ); + assert!( + ddl_lock_is_free(&url, table), + "the DDL advisory lock outlived a failed create" + ); + } +} From ed4d9dfa0e3f24cf29018eb5b9b7c05583c6fbe5 Mon Sep 17 00:00:00 2001 From: Michael Moffett <131298582+michael-moffett@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:00:08 -0600 Subject: [PATCH 10/13] feat(cli): ask before installing the Solana dev skill on first scaffold (#761) Co-authored-by: Micaiah Reid --- crates/cli/src/cli/simnet/startup.rs | 51 ++++- crates/cli/src/scaffold/mod.rs | 328 ++++++++++++++++++++++++++- 2 files changed, 371 insertions(+), 8 deletions(-) diff --git a/crates/cli/src/cli/simnet/startup.rs b/crates/cli/src/cli/simnet/startup.rs index 3ead555a6..a6fafcef2 100644 --- a/crates/cli/src/cli/simnet/startup.rs +++ b/crates/cli/src/cli/simnet/startup.rs @@ -28,8 +28,8 @@ use super::{ use crate::{ runbook::{execute_in_memory_runbook, execute_on_disk_runbook}, scaffold::{ - ProgramFrameworkData, detect_program_frameworks, scaffold_iac_layout, - scaffold_in_memory_iac, + DevSkillInstall, DevSkillInstallOutcome, ProgramFrameworkData, detect_program_frameworks, + scaffold_iac_layout, scaffold_in_memory_iac, }, }; @@ -202,6 +202,7 @@ struct StartupPlan { on_disk_runbook_data: Option<(FileLocation, Vec)>, in_memory_runbook_data: Option<(String, RunbookSources, WorkspaceManifest)>, runbook_input: Vec, + dev_skill_install: Option, } /// Inspects the project, scaffolds runbooks as the execution mode requires, @@ -222,6 +223,7 @@ async fn plan_startup( let mut on_disk_runbook_data = None; let mut in_memory_runbook_data = None; let mut clone_pubkeys = vec![]; + let mut dev_skill_install = None; let runbook_input = cmd.project.runbook_input.clone(); let mode = RunbookExecutionMode::from_inputs( @@ -280,7 +282,7 @@ async fn plan_startup( match mode { RunbookExecutionMode::ScaffoldOnDisk => { - scaffold_iac_layout( + dev_skill_install = scaffold_iac_layout( &framework, &programs, &base_location, @@ -328,6 +330,7 @@ async fn plan_startup( on_disk_runbook_data, in_memory_runbook_data, runbook_input, + dev_skill_install, }) } @@ -351,6 +354,7 @@ pub(super) async fn plan_and_dispatch_startup( on_disk_runbook_data, in_memory_runbook_data, runbook_input, + dev_skill_install, } = plan_startup(cmd, simnet_events_tx) .await .map_err(StartupPlanFailure::Planning)?; @@ -430,6 +434,12 @@ pub(super) async fn plan_and_dispatch_startup( } } + if let Some(install) = dev_skill_install { + let events_tx = simnet_events_tx.clone(); + let _ = hiro_system_kit::thread_named("Dev Skill Install Report") + .spawn(move || report_dev_skill_install(install.outcome(), &events_tx)); + } + if cmd.project.watch { // The watcher is a dev convenience; the startup tasks are // already dispatched and may legitimately reach Ready, so a watcher @@ -452,6 +462,15 @@ pub(super) async fn plan_and_dispatch_startup( Ok(progress_rx) } +fn report_dev_skill_install(outcome: DevSkillInstallOutcome, events_tx: &SimnetEventsTx) { + match outcome { + DevSkillInstallOutcome::Installed => events_tx.info("The Solana dev skill was installed"), + DevSkillInstallOutcome::Failed(reason) => { + events_tx.warn(format!("The Solana dev skill was not installed: {reason}")); + } + } +} + /// Watches the deploy-artifacts directory and re-executes the startup /// runbooks whenever a `.so` file is created or modified. fn spawn_artifact_watcher( @@ -594,7 +613,9 @@ fn assemble_runbook_execution_futures( #[cfg(test)] mod tests { - use super::RunbookExecutionMode; + use surfpool_types::{SimnetEvent, SimnetEventsTx}; + + use super::{DevSkillInstallOutcome, RunbookExecutionMode, report_dev_skill_install}; /// A project that already has a `txtx.yml` executes it as written. Framework /// detection contributes clone addresses on that path, but nothing the @@ -644,6 +665,28 @@ mod tests { } } + #[test] + fn both_install_outcomes_reach_the_user() { + let (events_tx, events_rx) = SimnetEventsTx::unbounded(); + + let success = "The Solana dev skill was installed"; + report_dev_skill_install(DevSkillInstallOutcome::Installed, &events_tx); + match events_rx.try_recv() { + Ok(SimnetEvent::InfoLog(_, message)) => assert!(message.contains(success), "{message}"), + other => panic!("a successful install was not reported: {other:?}"), + } + + let failure = "installer exited with status 1: YAML parse error"; + report_dev_skill_install( + DevSkillInstallOutcome::Failed(failure.to_string()), + &events_tx, + ); + match events_rx.try_recv() { + Ok(SimnetEvent::WarnLog(_, message)) => assert!(message.contains(failure), "{message}"), + other => panic!("a failed install was not reported: {other:?}"), + } + } + mod startup_watchdog { use std::time::{Duration, Instant}; diff --git a/crates/cli/src/scaffold/mod.rs b/crates/cli/src/scaffold/mod.rs index 33dffe4e2..e4e0a979c 100644 --- a/crates/cli/src/scaffold/mod.rs +++ b/crates/cli/src/scaffold/mod.rs @@ -1,6 +1,8 @@ use std::{ env, fs::{self, File}, + path::Path, + process::{Child, Command, Output, Stdio}, }; use dialoguer::{Confirm, Input, MultiSelect, console::Style, theme::ColorfulTheme}; @@ -18,7 +20,10 @@ use txtx_core::{ types::RunbookSources, }; -use crate::{cli::DEFAULT_SOLANA_KEYPAIR_PATH, types::Framework}; +use crate::{ + cli::{DEFAULT_SOLANA_KEYPAIR_PATH, get_home_dir}, + types::Framework, +}; pub const SURFPOOL_README_TEMPLATE: &str = include_str!("./templates/readme.md.mst"); @@ -134,6 +139,145 @@ impl ProgramMetadata { } } +const DEV_SKILL_REPO: &str = "https://github.com/solana-foundation/solana-dev-skill"; + +const DEV_SKILL_INSTALLER: &str = "skills@1.5.23"; + +/// Anything else also writes `.claude/`, `agent/` and per-agent lock files. +const DEV_SKILL_AGENT: &str = "universal"; + +const DEV_SKILL_DIR: &str = ".agents/skills/solana-dev"; + +const DEV_SKILL_DECLINED_MARKER: &str = ".config/surfpool/dev-skill-declined"; + +const DEV_SKILL_ACCEPTED_MARKER: &str = ".config/surfpool/dev-skill-accepted"; + +fn dev_skill_install_command(base_location: &FileLocation) -> Command { + let mut command = Command::new("npx"); + command.args([ + "-y", + DEV_SKILL_INSTALLER, + "add", + DEV_SKILL_REPO, + "--skill", + "*", + "--agent", + DEV_SKILL_AGENT, + "-y", + "--global", + ]); + command.current_dir(base_location.expect_path_buf()); + command +} + +#[derive(Debug)] +pub enum DevSkillInstallOutcome { + Installed, + /// Carries the installer's own output. + Failed(String), +} + +/// The holder is responsible for reporting the outcome. +#[must_use] +pub struct DevSkillInstall(Child); + +impl DevSkillInstall { + /// Blocks until the install finishes. + pub fn outcome(self) -> DevSkillInstallOutcome { + match self.0.wait_with_output() { + Ok(output) if output.status.success() => DevSkillInstallOutcome::Installed, + Ok(output) => DevSkillInstallOutcome::Failed(installer_failure(&output)), + Err(e) => DevSkillInstallOutcome::Failed(e.to_string()), + } + } +} + +/// Errs only when the installer could not be started at all. +fn spawn_dev_skill_install(mut command: Command) -> Result { + command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map(DevSkillInstall) + .map_err(|e| e.to_string()) +} + +fn installer_failure(output: &Output) -> String { + let mut reason = match output.status.code() { + Some(code) => format!("installer exited with status {code}"), + None => "installer was terminated".to_string(), + }; + let mut details = String::from_utf8_lossy(&output.stderr).trim().to_string(); + if details.is_empty() { + details = String::from_utf8_lossy(&output.stdout).trim().to_string(); + } + if !details.is_empty() { + reason = format!("{reason}: {details}"); + } + reason +} + +fn dev_skill_installed(base: &Path, home: &Path) -> bool { + base.join(DEV_SKILL_DIR).exists() || home.join(DEV_SKILL_DIR).exists() +} + +/// Returns the answer the user gave last time, or `None` if they have not been asked. +fn dev_skill_answer(home: &Path) -> Option { + if home.join(DEV_SKILL_DECLINED_MARKER).exists() { + Some(false) + } else if home.join(DEV_SKILL_ACCEPTED_MARKER).exists() { + Some(true) + } else { + None + } +} + +fn prompt_for_dev_skill(theme: &ColorfulTheme) -> Option { + Confirm::with_theme(theme) + .with_prompt(format!( + "Install the solana-dev skill? This will write to the {DEV_SKILL_DIR} directory." + )) + .default(true) + .interact() + .ok() +} + +fn record_dev_skill_answer(home: &Path, consented: bool) { + let marker = home.join(match consented { + true => DEV_SKILL_ACCEPTED_MARKER, + false => DEV_SKILL_DECLINED_MARKER, + }); + if let Some(parent) = marker.parent() { + let _ = fs::create_dir_all(parent); + } + let _ = File::create(marker); +} + +fn dev_skill_install_if_wanted( + base_location: &FileLocation, + base: &Path, + home: &Path, + auto_accept: bool, + ask: impl FnOnce() -> Option, +) -> Option { + if dev_skill_installed(base, home) { + return None; + } + let consented = match dev_skill_answer(home) { + Some(recorded) => recorded, + None if auto_accept => false, + None => match ask() { + Some(answer) => { + record_dev_skill_answer(home, answer); + answer + } + None => false, + }, + }; + consented.then(|| dev_skill_install_command(base_location)) +} + pub fn scaffold_in_memory_iac( framework: &Framework, programs: &[ProgramMetadata], @@ -205,7 +349,7 @@ pub fn scaffold_iac_layout( programs: &[ProgramMetadata], base_location: &FileLocation, auto_generate_runbooks: bool, -) -> Result<(), String> { +) -> Result, String> { let mut target_location = base_location.clone(); target_location.append_path("target")?; @@ -428,7 +572,7 @@ pub fn scaffold_iac_layout( // "file {} already exists. choose a different runbook name, or rename the existing file", // runbook_file_location.to_string() // )) - return Ok(()); + return Ok(None); } false => { // write main.tx @@ -512,5 +656,181 @@ pub fn scaffold_iac_layout( println!("Deployment canceled"); } - Ok(()) + let home = get_home_dir(); + let base = base_location.expect_path_buf(); + let dev_skill_install = match dev_skill_install_if_wanted( + base_location, + &base, + Path::new(&home), + auto_generate_runbooks, + || prompt_for_dev_skill(&theme), + ) { + Some(command) => match spawn_dev_skill_install(command) { + Ok(install) => { + println!( + "{} {}", + green!("Installing in the background"), + DEV_SKILL_DIR + ); + Some(install) + } + Err(e) => { + println!( + "{} {}: {}", + red!("Could not start the installer for"), + DEV_SKILL_DIR, + e + ); + None + } + }, + None => None, + }; + + Ok(dev_skill_install) +} + +#[cfg(test)] +mod tests { + use std::{ + fs::{self, File}, + process::Command, + }; + + use tempfile::TempDir; + + use super::{ + DEV_SKILL_ACCEPTED_MARKER, DEV_SKILL_DECLINED_MARKER, DEV_SKILL_DIR, + DevSkillInstallOutcome, FileLocation, dev_skill_answer, dev_skill_install_if_wanted, + spawn_dev_skill_install, + }; + + fn scratch() -> (TempDir, TempDir, FileLocation) { + let base = TempDir::new().unwrap(); + let home = TempDir::new().unwrap(); + let location = FileLocation::from_path_string(base.path().to_str().unwrap()).unwrap(); + (base, home, location) + } + + #[cfg(unix)] + fn sh(script: &str) -> Command { + let mut command = Command::new("sh"); + command.args(["-c", script]); + command + } + + #[test] + fn an_installed_skill_is_left_alone() { + for (in_base, in_home) in [(true, false), (false, true)] { + let (base, home, location) = scratch(); + let root = if in_base { base.path() } else { home.path() }; + fs::create_dir_all(root.join(DEV_SKILL_DIR)).unwrap(); + assert!( + dev_skill_install_if_wanted(&location, base.path(), home.path(), true, || { + panic!("an installed skill must not prompt") + }) + .is_none(), + "installed in base={in_base} home={in_home}" + ); + } + } + + #[test] + fn the_yes_flag_uses_the_recorded_answer() { + for declined in [true, false] { + let (base, home, location) = scratch(); + let marker = home.path().join(match declined { + true => DEV_SKILL_DECLINED_MARKER, + false => DEV_SKILL_ACCEPTED_MARKER, + }); + fs::create_dir_all(marker.parent().unwrap()).unwrap(); + File::create(&marker).unwrap(); + let install = + dev_skill_install_if_wanted(&location, base.path(), home.path(), true, || { + panic!("--yes must not prompt") + }); + assert_eq!(install.is_none(), declined, "declined={declined}"); + } + } + + #[test] + fn the_yes_flag_alone_installs_nothing_and_records_nothing() { + let (base, home, location) = scratch(); + assert!( + dev_skill_install_if_wanted(&location, base.path(), home.path(), true, || { + panic!("--yes must not prompt") + }) + .is_none() + ); + assert!(dev_skill_answer(home.path()).is_none()); + } + + #[test] + fn a_declined_prompt_starts_no_install_and_is_remembered() { + let (base, home, location) = scratch(); + assert!( + dev_skill_install_if_wanted(&location, base.path(), home.path(), false, || Some(false)) + .is_none() + ); + assert!(home.path().join(DEV_SKILL_DECLINED_MARKER).exists()); + assert!( + dev_skill_install_if_wanted(&location, base.path(), home.path(), false, || { + panic!("the recorded decline must not prompt again") + }) + .is_none() + ); + } + + #[test] + fn an_accepted_prompt_installs_and_is_remembered() { + let (base, home, location) = scratch(); + assert!( + dev_skill_install_if_wanted(&location, base.path(), home.path(), false, || Some(true)) + .is_some() + ); + assert!( + dev_skill_install_if_wanted(&location, base.path(), home.path(), false, || { + panic!("the recorded accept must not prompt again") + }) + .is_some() + ); + } + + #[test] + fn an_undisplayable_prompt_installs_nothing_and_records_nothing() { + let (base, home, location) = scratch(); + assert!( + dev_skill_install_if_wanted(&location, base.path(), home.path(), false, || None) + .is_none() + ); + assert!(dev_skill_answer(home.path()).is_none()); + } + + #[cfg(unix)] + #[test] + fn failed_install_reports_the_error() { + for script in [ + "echo 'YAML parse error' >&2; exit 1", + "echo 'YAML parse error'; exit 1", + ] { + let install = spawn_dev_skill_install(sh(script)).expect("the installer started"); + match install.outcome() { + DevSkillInstallOutcome::Failed(reason) => { + assert_eq!(reason, "installer exited with status 1: YAML parse error"); + } + DevSkillInstallOutcome::Installed => panic!("{script}: reported success"), + } + } + } + + #[cfg(unix)] + #[test] + fn successful_install_reports_installed() { + let install = spawn_dev_skill_install(sh("echo 'copied 34 files'; exit 0")) + .expect("the installer started"); + assert!( + matches!(install.outcome(), DevSkillInstallOutcome::Installed), + "a clean install was reported as a failure" + ); + } } From 3c143adeb95a0d73d642c6ed4c11b47ff1ec2c78 Mon Sep 17 00:00:00 2001 From: xternet Date: Thu, 3 Sep 2026 20:06:19 +0200 Subject: [PATCH 11/13] fix(rpc): omit empty simulateTransaction return data (#795) --- crates/core/src/rpc/full.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/core/src/rpc/full.rs b/crates/core/src/rpc/full.rs index 333924a17..3f326cba7 100644 --- a/crates/core/src/rpc/full.rs +++ b/crates/core/src/rpc/full.rs @@ -27,7 +27,6 @@ use solana_pubkey::Pubkey; use solana_rpc_client_api::response::Response as RpcResponse; use solana_sdk_ids::compute_budget; use solana_signature::Signature; -use solana_system_interface::program as system_program; use solana_transaction_error::TransactionError; use solana_transaction_status::{ EncodedConfirmedTransactionWithStatusMeta, EncodedTransactionWithStatusMeta, @@ -2722,9 +2721,7 @@ fn get_simulate_transaction_result( }, logs: Some(metadata.logs.clone()), replacement_blockhash, - return_data: if metadata.return_data.program_id == system_program::id() - && metadata.return_data.data.is_empty() - { + return_data: if metadata.return_data.data.is_empty() { None } else { Some(metadata.return_data.clone().into()) @@ -2834,6 +2831,28 @@ mod tests { VersionedTransaction::try_new(msg, signers).unwrap() } + #[test] + fn simulate_transaction_omits_only_empty_return_data() { + let mut metadata = TransactionMetadata::default(); + metadata.return_data.program_id = Pubkey::new_unique(); + let message = VersionedMessage::Legacy(LegacyMessage::default()); + + let result = get_simulate_transaction_result( + metadata, None, None, None, false, &message, None, None, + ); + + assert!(result.return_data.is_none()); + + let mut metadata = TransactionMetadata::default(); + metadata.return_data.program_id = Pubkey::new_unique(); + metadata.return_data.data = vec![1]; + let result = get_simulate_transaction_result( + metadata, None, None, None, false, &message, None, None, + ); + + assert!(result.return_data.is_some()); + } + async fn send_and_await_transaction( tx: VersionedTransaction, setup: TestSetup, From 704c6e372cc4809f8445e04fecae0804446e96d3 Mon Sep 17 00:00:00 2001 From: Michael Moffett Date: Thu, 3 Sep 2026 14:56:00 -0600 Subject: [PATCH 12/13] fix(rpc): handle JSON-RPC batch requests SurfpoolMiddleware::on_request bound Request::Single(Call::MethodCall) and rejected everything else, so every batch was discarded before the handler saw it. jsonrpc-core already parses Request::Batch and its handler already implements the batch response rules; the middleware was the only thing in the way. Batches now go through the same surfnet_-prefix cheatcode gate as single requests, one element at a time. A gated element fails on its own rather than failing the batch, and a gated notification is answered by nothing, as any notification is. Closes #717 --- crates/core/src/rpc/mod.rs | 236 ++++++++++++++++++++++++++++++------- 1 file changed, 192 insertions(+), 44 deletions(-) diff --git a/crates/core/src/rpc/mod.rs b/crates/core/src/rpc/mod.rs index e5cb3d36a..a23cba90a 100644 --- a/crates/core/src/rpc/mod.rs +++ b/crates/core/src/rpc/mod.rs @@ -6,7 +6,8 @@ use std::{ use blake3::Hash; use crossbeam_channel::Sender; use jsonrpc_core::{ - BoxFuture, Error, ErrorCode, FutureResponse, Metadata, Middleware, Request, Response, + BoxFuture, Call, Error, ErrorCode, FutureResponse, Metadata, Middleware, Output, Request, + Response, futures::{FutureExt, future::Either}, middleware, }; @@ -138,6 +139,87 @@ impl SurfpoolMiddleware { plugin_commands_tx, } } + + /// `Some` when the call must not reach the handler. Cheatcode gating sits above the method + /// table rather than in it, so it has to run once per batch element, not once per request. + fn disabled_cheatcode_error(&self, method_name: &str) -> Option { + if !method_name.starts_with("surfnet_") { + return None; + } + + let Ok(cheatcode_config) = self.cheatcode_config.lock() else { + warn!("Request rejected due to cheatcode being disabled"); + return Some(Error { + code: ErrorCode::InternalError, + message: "An internal server error occured".to_string(), + data: None, + }); + }; + + if !cheatcode_config.is_cheatcode_disabled(&method_name.to_string()) { + return None; + } + + warn!("Request rejected due to cheatcode rpc method being disabled"); + Some(Error { + code: ErrorCode::InvalidRequest, + message: format!("Cheatcode rpc method: {method_name} is currently disabled"), + data: None, + }) + } + + fn dispatch_batch( + &self, + calls: Vec, + meta: Option, + next: F, + ) -> Either + where + F: FnOnce(Request, Option) -> X + Send, + X: Future> + Send + 'static, + { + let mut forwarded = Vec::with_capacity(calls.len()); + let mut rejected = Vec::new(); + + for call in calls { + // A malformed element carries no method to gate; the handler answers it per element. + let method_name = match &call { + Call::MethodCall(method_call) => Some(method_call.method.as_str()), + Call::Notification(notification) => Some(notification.method.as_str()), + Call::Invalid { .. } => None, + }; + + match method_name.and_then(|name| self.disabled_cheatcode_error(name)) { + None => forwarded.push(call), + // A notification is answered by nothing at all, gated or not. + Some(error) => { + if let Call::MethodCall(method_call) = call { + rejected.push(Output::from( + Err(error), + method_call.id, + method_call.jsonrpc, + )); + } + } + } + } + + if forwarded.is_empty() { + let response = (!rejected.is_empty()).then_some(Response::Batch(rejected)); + return Either::Left(Box::pin(async move { response })); + } + + // Order is not part of the batch contract: clients correlate on `id`. + Either::Left(Box::pin(next(Request::Batch(forwarded), meta).map( + move |res| match res { + Some(Response::Batch(mut outputs)) => { + outputs.extend(rejected); + Some(Response::Batch(outputs)) + } + _ => (!rejected.is_empty()).then_some(Response::Batch(rejected)), + }, + ))) + } } impl Middleware> for SurfpoolMiddleware { @@ -154,7 +236,21 @@ impl Middleware> for SurfpoolMiddleware { F: FnOnce(Request, Option) -> X + Send, X: Future> + Send + 'static, { - let Request::Single(jsonrpc_core::Call::MethodCall(ref method_call)) = request else { + let meta = Some(RunloopContext { + id: None, + svm_locker: self.surfnet_svm.clone(), + simnet_commands_tx: self.simnet_commands_tx.clone(), + remote_rpc_client: self.remote_rpc_client.clone(), + rpc_config: self.config.clone(), + cheatcode_config: self.cheatcode_config.clone(), + plugin_commands_tx: self.plugin_commands_tx.clone(), + }); + + let Request::Single(Call::MethodCall(ref method_call)) = request else { + if let Request::Batch(calls) = request { + return self.dispatch_batch(calls, meta, next); + } + let error = Response::from( Error { code: ErrorCode::InvalidRequest, @@ -171,48 +267,9 @@ impl Middleware> for SurfpoolMiddleware { let method_name = method_call.method.clone(); debug!("Processing request '{}'", method_name); - let meta = Some(RunloopContext { - id: None, - svm_locker: self.surfnet_svm.clone(), - simnet_commands_tx: self.simnet_commands_tx.clone(), - remote_rpc_client: self.remote_rpc_client.clone(), - rpc_config: self.config.clone(), - cheatcode_config: self.cheatcode_config.clone(), - plugin_commands_tx: self.plugin_commands_tx.clone(), - }); - - // All surfnet cheatcodes will start with surfnet. If the request is a cheatcode, make sure it isn't disabled. - if method_name.starts_with("surfnet_") - && let Some(meta_val) = meta.clone() - { - let Ok(meta_val) = meta_val.cheatcode_config.lock() else { - let error = Response::from( - Error { - code: ErrorCode::InternalError, - message: "An internal server error occured".to_string(), - data: None, - }, - None, - ); - warn!("Request rejected due to cheatcode being disabled"); - - return Either::Left(Box::pin(async move { Some(error) })); - }; - if meta_val.is_cheatcode_disabled(&method_name) { - let error = Response::from( - Error { - code: ErrorCode::InvalidRequest, - message: format!( - "Cheatcode rpc method: {method_name} is currently disabled" - ), - data: None, - }, - None, - ); - warn!("Request rejected due to cheatcode rpc method being disabled"); - - return Either::Left(Box::pin(async move { Some(error) })); - } + if let Some(error) = self.disabled_cheatcode_error(&method_name) { + let error = Response::from(error, None); + return Either::Left(Box::pin(async move { Some(error) })); } Either::Left(Box::pin(next(request, meta).map(move |res| { @@ -374,3 +431,94 @@ pub fn not_implemented_err_async(method: &str) -> BoxFuture> }) }) } + +#[cfg(test)] +mod tests { + use jsonrpc_core::{MetaIoHandler, Value}; + use serde_json::json; + + use super::*; + + /// A handler carrying the real middleware, one plain method and one cheatcode, so the tests + /// exercise the batch path end to end rather than the middleware in isolation. + fn test_handler() -> MetaIoHandler, SurfpoolMiddleware> { + let (surfnet_svm, _events_rx, _) = SurfnetSvm::default(); + let (simnet_commands_tx, _rx) = crossbeam_channel::unbounded(); + let (plugin_commands_tx, _rx) = crossbeam_channel::unbounded(); + + let middleware = SurfpoolMiddleware::new( + SurfnetSvmLocker::new(surfnet_svm), + &simnet_commands_tx, + &RpcConfig::default(), + &None, + plugin_commands_tx, + ); + middleware + .cheatcode_config + .lock() + .unwrap() + .disable_cheatcode(&"surfnet_setAccount".to_string()) + .unwrap(); + + let mut io = MetaIoHandler::with_middleware(middleware); + io.add_method_with_meta("getSlot", |_params, _meta| async { Ok(Value::from(45)) }); + io.add_method_with_meta("surfnet_setAccount", |_params, _meta| async { + Ok(Value::Null) + }); + io + } + + #[tokio::test] + async fn batch_of_method_calls_is_answered_with_an_array() { + let request = r#"[{"jsonrpc":"2.0","id":1,"method":"getSlot"},{"jsonrpc":"2.0","id":2,"method":"getSlot"}]"#; + + let response = test_handler().handle_request(request, None).await.unwrap(); + + assert_eq!( + serde_json::from_str::(&response).unwrap(), + json!([ + {"jsonrpc": "2.0", "result": 45, "id": 1}, + {"jsonrpc": "2.0", "result": 45, "id": 2} + ]) + ); + } + + #[tokio::test] + async fn notifications_are_omitted_from_the_batch_response() { + let request = + r#"[{"jsonrpc":"2.0","method":"getSlot"},{"jsonrpc":"2.0","id":2,"method":"getSlot"}]"#; + + let response = test_handler().handle_request(request, None).await.unwrap(); + + assert_eq!( + serde_json::from_str::(&response).unwrap(), + json!([{"jsonrpc": "2.0", "result": 45, "id": 2}]) + ); + } + + #[tokio::test] + async fn all_notification_batch_is_answered_with_nothing() { + let request = + r#"[{"jsonrpc":"2.0","method":"getSlot"},{"jsonrpc":"2.0","method":"getSlot"}]"#; + + assert_eq!(test_handler().handle_request(request, None).await, None); + } + + #[tokio::test] + async fn a_rejected_element_fails_alone() { + let request = r#"[{"jsonrpc":"2.0","id":1,"method":"getSlot"},{"jsonrpc":"2.0","id":2,"method":"surfnet_setAccount"}]"#; + + let response = test_handler().handle_request(request, None).await.unwrap(); + + assert_eq!( + serde_json::from_str::(&response).unwrap(), + json!([ + {"jsonrpc": "2.0", "result": 45, "id": 1}, + {"jsonrpc": "2.0", "error": { + "code": -32600, + "message": "Cheatcode rpc method: surfnet_setAccount is currently disabled" + }, "id": 2} + ]) + ); + } +} From 7fa6f1a13d6c06a2d79c10f76e09ea18779fc464 Mon Sep 17 00:00:00 2001 From: Michael Moffett Date: Fri, 4 Sep 2026 14:12:42 -0600 Subject: [PATCH 13/13] fix(rpc): answer an empty JSON-RPC batch with one Invalid Request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An empty array `[]` deserialized to Request::Batch(vec![]), reached dispatch_batch, and fell out of `if forwarded.is_empty()` as None, so the server sent no response at all. At 3c143ad the same input was answered -32600 with a null id, making this a regression against base as well as a JSON-RPC 2.0 §6 MUST ("the Server MUST respond with a single Response object" for an array with no values). F1: gate the batch arm on !calls.is_empty(), routing `[]` to the existing rejection arm three lines below, so the restored answer is byte identical to base. F2: a fifth test, an_empty_batch_is_answered_with_one_invalid_request, for the restored spec behaviour. It fails before F1 (unwrap on None) and passes after. Applied verbatim from the S4b code-quality worklist. Ticket: T-DIR-P040-S4B-A-APPLY-F1-F2-717-2026-09-04 --- crates/core/src/rpc/mod.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/core/src/rpc/mod.rs b/crates/core/src/rpc/mod.rs index a23cba90a..616ee9c30 100644 --- a/crates/core/src/rpc/mod.rs +++ b/crates/core/src/rpc/mod.rs @@ -247,7 +247,11 @@ impl Middleware> for SurfpoolMiddleware { }); let Request::Single(Call::MethodCall(ref method_call)) = request else { - if let Request::Batch(calls) = request { + // JSON-RPC 2.0 §6: an empty array is not a batch and answers with one Invalid + // Request object, which is what the arm below already returns. + if let Request::Batch(calls) = request + && !calls.is_empty() + { return self.dispatch_batch(calls, meta, next); } @@ -521,4 +525,17 @@ mod tests { ]) ); } + + #[tokio::test] + async fn an_empty_batch_is_answered_with_one_invalid_request() { + let response = test_handler().handle_request("[]", None).await.unwrap(); + + assert_eq!( + serde_json::from_str::(&response).unwrap(), + json!({ + "error": {"code": -32600, "message": "Only method calls are supported"}, + "id": null + }) + ); + } }