Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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(|_| {
Comment thread
citizen-stig marked this conversation as resolved.
Outdated
SimulateError::InvalidInput("failed to parse target address".to_owned())
})?;
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 @@ -175,10 +175,13 @@ 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.
/// 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