Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/full-node/sov-api-spec/openapi-v3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,10 @@ components:
type: object
description: Optional uniqueness data for the transaction
additionalProperties: true
target_address:
type: string
nullable: true
description: Optional target address for execution; null or omission uses default routing
required:
- sender
- call
Expand Down
23 changes: 18 additions & 5 deletions crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,23 +304,34 @@ impl<S: Spec, R: Runtime<S>> SovereignSimulate<S, R> {
&self,
params: &SimulateParameters,
state: &mut StateCheckpoint<S>,
) -> AuthorizationData<S> {
let credential_id = CredentialId::from_str(&params.sender).unwrap();
) -> Result<AuthorizationData<S>, SimulateError> {
let credential_id = CredentialId::from_str(&params.sender).map_err(|_| {
SimulateError::InvalidInput("failed to parse sender credential id".to_owned())
})?;
let address = params
.target_address
.as_deref()
.map(S::Address::from_str)
.transpose()
.map_err(|e| {
SimulateError::InvalidInput(format!("failed to parse target address: {e:?}"))
})?;
let uniqueness = params.uniqueness.unwrap_or_else(|| {
let generation = Uniqueness::<S>::default()
.next_generation(&credential_id, state)
.unwrap();
UniquenessData::Generation(generation)
});

AuthorizationData {
Ok(AuthorizationData {
tx_hash: NULL_TX_HASH,
non_malleable_hash: NULL_TX_HASH,
uniqueness,
credential_id,
default_address: credential_id.into(),
credentials: Credentials::new(credential_id),
}
address,
})
}

fn outcome(&self, result: ApplyTxResult<S>) -> SimulateOutcome<R::RuntimeEvent> {
Expand Down Expand Up @@ -402,6 +413,8 @@ pub struct SimulateParameters {
/// Optional uniqueness data for the transaction.
/// If not provided a valid uniqueness will be used.
pub uniqueness: Option<UniquenessData>,
/// Optional target address for execution. If not provided, default routing is used.
pub target_address: Option<String>,
}

impl<S: Spec, R: Runtime<S>> SimulateEndpoint for SovereignSimulate<S, R> {
Expand All @@ -427,7 +440,7 @@ impl<S: Spec, R: Runtime<S>> SimulateEndpoint for SovereignSimulate<S, R> {
.chain_state()
.base_fee_per_gas(&mut accessor)
.ok_or(SimulateError::GasPriceRetrieval)?;
let auth_data = state.authorization_data(&params, &mut accessor);
let auth_data = state.authorization_data(&params, &mut accessor)?;
let sequencer = state.sequencer(params.sequencer.unwrap_or_default())?;
let auth_tx_data =
AuthenticatedTransactionData(state.tx_details(params.tx_details.unwrap_or_default())?);
Expand Down
93 changes: 91 additions & 2 deletions crates/full-node/sov-rollup-apis/tests/integration/rest_api.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use sov_api_spec::types;
use sov_bank::{config_gas_token_id, Bank};
use sov_modules_api::prelude::tokio::{self};
use sov_modules_api::{Amount, Gas, GasArray, GasSpec, Spec};
use sov_modules_api::sov_universal_wallet::schema::RollupRoots;
use sov_modules_api::{
get_runtime_schema, Amount, CredentialId, Gas, GasArray, GasSpec, Runtime, Spec,
};
use sov_rest_utils::json_obj;
use sov_rollup_interface::node::SyncStatus;
use sov_test_utils::{AsUser, TestUser, TransactionTestCase};
use sov_test_utils::{
default_test_tx_details, AsUser, TestUser, TransactionTestCase, TransactionType,
};

use crate::{TestData, RT, S};

Expand Down Expand Up @@ -167,6 +172,90 @@ async fn test_simulation_success() {
);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_simulation_success_with_target_address() {
let mut data = TestData::setup().await;
let delegated_credential: CredentialId = [9u8; 32].into();
let registration_call = json_obj!({
"accounts": {
"insert_credential_id": delegated_credential,
},
});
let schema = get_runtime_schema::<S, RT>().unwrap();
let registration_call_bytes = schema
.json_to_borsh(
schema
.rollup_expected_index(RollupRoots::RuntimeCall)
.unwrap(),
&serde_json::to_string(&registration_call).unwrap(),
)
.unwrap();

data.runner.execute_transaction(TransactionTestCase {
input: TransactionType::Plain {
message: RT::decode_call(&registration_call_bytes).unwrap(),
key: data.user.private_key().clone(),
details: default_test_tx_details::<S>(),
},
assert: Box::new(move |result, _state| {
assert!(
result.tx_receipt.is_successful(),
"The credential registration should have succeeded"
);
}),
});
data.send_storage();

let target_address = data.user.address();
let receiver = TestUser::<S>::generate_with_default_balance().address();
let call = sov_bank::CallMessage::<S>::Transfer {
to: receiver,
coins: sov_bank::Coins {
amount: Amount::new(1000),
token_id: config_gas_token_id(),
},
};
let params = json_obj!({
"sender": delegated_credential.to_string(),
"target_address": target_address.to_string(),
"call": {
"bank": call,
},
});
let client = reqwest::Client::new();

let response = client
.post(format!("http://{}/rollup/simulate", data.axum_addr))
.json(&params)
.send()
.await
.unwrap();
let actual = response.json::<serde_json::Value>().await.unwrap();

assert_eq!(actual["outcome"], "success");
assert_eq!(
actual["events"][0],
serde_json::json!({
"key": "Bank/TokenTransferred",
"module": "Bank",
"value": {
"token_transferred": {
"from": {
"user": target_address
},
"to": {
"user": receiver
},
"coins": {
"amount": "1000",
"token_id": "token_1nyl0e0yweragfsatygt24zmd8jrr2vqtvdfptzjhxkguz2xxx3vs0y07u7"
}
}
}
})
);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_simulation_fail() {
let data = TestData::setup().await;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,12 @@ pub fn sign_utx_in_place<S: Spec, RT: Runtime<S>>(

/// Signs a V1 (multisig) unsigned transaction with the given private key using EIP712.
/// The credential_address is computed from the provided multisig.
/// `target_address` is forwarded into the signed `UnsignedTransactionV1`;
/// pass `None` to match pre-target routing or `Some(X)` to sign for a specific target account.
pub fn sign_utx_v1_in_place<S: Spec, RT: Runtime<S>>(
utx: &UnsignedTransactionV0<RT, S>,
multisig: &Multisig<<S::CryptoSpec as CryptoSpec>::PublicKey>,
target_address: Option<S::Address>,
private_key: &<S::CryptoSpec as CryptoSpec>::PrivateKey,
) -> <S::CryptoSpec as CryptoSpec>::Signature {
let schema = TestSchemaProvider::get_schema();
Expand All @@ -197,6 +200,7 @@ pub fn sign_utx_v1_in_place<S: Spec, RT: Runtime<S>>(
uniqueness: utx.uniqueness,
details: utx.details.clone(),
credential_address,
target_address,
});
let utx_bytes = borsh::to_vec(&utx_enum).expect("Failed to serialize unsigned transaction");
let eip712_signing_data = schema
Expand Down Expand Up @@ -303,16 +307,17 @@ fn test_multisig_signature_verification() {

// Generate a signature from a random private key that's not part of the multisig. We'll use this in some of the test cases.
let random_private_key = TestPrivateKey::generate();
let target = None;
let make_multisig_tx = |generation| {
let utx = create_utx_with_generation::<S, RT>(encode_message::<_, RT>(), generation);
let signatures = multisig_keys
.iter()
.map(|key| sign_utx_v1_in_place(&utx, &multisig, key))
.map(|key| sign_utx_v1_in_place(&utx, &multisig, target, key))
.collect::<Vec<_>>();
let random_signature = sign_utx_v1_in_place(&utx, &multisig, &random_private_key);
let random_signature = sign_utx_v1_in_place(&utx, &multisig, target, &random_private_key);

(
utx.to_multisig_tx(multisig.clone()),
utx.to_multisig_tx(multisig.clone(), target),
signatures,
random_signature,
)
Expand Down
Loading
Loading