Skip to content
24 changes: 24 additions & 0 deletions crates/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use solana_clock::Slot;
use solana_pubkey::Pubkey;
use solana_transaction::TransactionError;
use solana_transaction_status::EncodeError;
use surfpool_types::ScenarioError;

use crate::storage::StorageError;

Expand Down Expand Up @@ -470,6 +471,21 @@ impl SurfpoolError {
error.message = format!("Expected profile not found for key {key}");
Self(error)
}

pub fn invalid_scenario(scenario_name: &str, failures: &[(String, ScenarioError)]) -> Self {
let mut error = Error::invalid_request();
let failure_messages = failures
.iter()
.map(|(id, err)| format!("override '{}': {}", id, err))
.collect::<Vec<_>>()
.join("; ");
let message = format!(
"Cannot register scenario '{}': {}",
scenario_name, failure_messages
);
error.message = message;
Self(error)
}
}

impl From<StorageError> for SurfpoolError {
Expand All @@ -496,6 +512,14 @@ impl From<TransactionError> for SurfpoolError {
}
}

impl From<ScenarioError> for SurfpoolError {
fn from(e: ScenarioError) -> Self {
let mut error = Error::invalid_request();
error.data = Some(json!(format!("Scenario error: {}", e)));
Self(error)
}
}

/// Error returned by [`crate::surfnet::svm::SurfnetSvm::airdrop`] when the
/// requested airdrop is rejected up front, before any synthetic transaction
/// is constructed or any account state is touched.
Expand Down
7 changes: 7 additions & 0 deletions crates/core/src/rpc/surfnet_cheatcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,13 @@ pub trait SurfnetCheatcodes {
/// ## Returns
/// A `RpcResponse<()>` indicating whether the Scenario registration was successful.
///
/// Registration validates every enabled override before scheduling anything: each
/// override's account address must resolve with the provided values. If any address
/// cannot resolve (a malformed pubkey, a PDA seed referencing a missing property, a
/// value of the wrong type), the request is rejected with an error message listing
/// each failing override id and its cause, and no overrides are scheduled. Disabled
/// overrides are not validated, since they are never materialized.
///
/// ## Example Request (with slot)
/// ```json
/// {
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/scenarios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ For custom protocols, an IDL can be registered at runtime using the [`surfnet_re
Scenarios can be registered at runtime using the [`surfnet_registerScenario`](https://docs.surfpool.run/rpc/cheatcodes#surfnet-registerscenario) RPC cheatcode.
This cheatcode takes in a scenario definition in JSON format, which includes the scenario name, description, and a list of overrides to apply to accounts.
Each override contains a map of the field in the account to override (as indexed in the IDL), and the value to apply for that key.
Registration validates every enabled override up front: each override's account address must resolve with the provided values, and a scenario containing an unresolvable address (a malformed pubkey, a PDA seed referencing a missing property, a value of the wrong type) is rejected with an error listing each failing override and its cause.

### Override Templates
Directly using the `surfnet_registerScenario` endpoint requires building out a map of account keys that are specific to the schema of the account that is being written to.
Expand Down
14 changes: 8 additions & 6 deletions crates/core/src/scenarios/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ impl TemplateRegistry {
};

// Convert all templates in the collection
let templates = collection.to_override_templates(idl);
let templates = match collection.to_override_templates(idl) {
Ok(t) => t,
Err(e) => panic!("unable to convert {} templates: {}", protocol_name, e),
};

// Register each template
for template in templates {
Expand Down Expand Up @@ -223,7 +226,7 @@ mod tests {
)]);
let bytes = derived_pda_seed
.to_bytes(Some(&values))
.unwrap_or_else(|| panic!("option {} did not resolve", option.id));
.unwrap_or_else(|e| panic!("option {} did not resolve: {e}", option.id));

assert_eq!(
Pubkey::try_from(bytes.as_slice()).expect("32 bytes"),
Expand Down Expand Up @@ -313,14 +316,13 @@ mod tests {
]);

assert!(
template.address.resolve(Some(&values)).is_some(),
template.address.resolve(Some(&values)).is_ok(),
"every seed resolves, so the pool address does too"
);

values.remove("config_index");
assert_eq!(
template.address.resolve(Some(&values)),
None,
assert!(
template.address.resolve(Some(&values)).is_err(),
"a seed that cannot resolve must not derive a shorter address"
);
}
Expand Down
Loading
Loading