simulateTransaction can now return accounts modified by the simulation

This commit is contained in:
Michael Vines
2021-05-25 16:44:18 -07:00
parent 54f0fc9f0f
commit cbce440af4
11 changed files with 184 additions and 49 deletions

View File

@ -48,7 +48,7 @@ pub enum UiAccountData {
Binary(String, UiAccountEncoding), Binary(String, UiAccountEncoding),
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum UiAccountEncoding { pub enum UiAccountEncoding {
Binary, // Legacy. Retained for RPC backwards compatibility Binary, // Legacy. Retained for RPC backwards compatibility
@ -62,7 +62,7 @@ pub enum UiAccountEncoding {
impl UiAccount { impl UiAccount {
pub fn encode<T: ReadableAccount>( pub fn encode<T: ReadableAccount>(
pubkey: &Pubkey, pubkey: &Pubkey,
account: T, account: &T,
encoding: UiAccountEncoding, encoding: UiAccountEncoding,
additional_data: Option<AccountAdditionalData>, additional_data: Option<AccountAdditionalData>,
data_slice_config: Option<UiDataSliceConfig>, data_slice_config: Option<UiDataSliceConfig>,
@ -224,7 +224,7 @@ mod test {
fn test_base64_zstd() { fn test_base64_zstd() {
let encoded_account = UiAccount::encode( let encoded_account = UiAccount::encode(
&Pubkey::default(), &Pubkey::default(),
AccountSharedData::from(Account { &AccountSharedData::from(Account {
data: vec![0; 1024], data: vec![0; 1024],
..Account::default() ..Account::default()
}), }),

View File

@ -1123,7 +1123,7 @@ fn process_show_account(
pubkey: account_pubkey.to_string(), pubkey: account_pubkey.to_string(),
account: UiAccount::encode( account: UiAccount::encode(
account_pubkey, account_pubkey,
account, &account,
UiAccountEncoding::Base64, UiAccountEncoding::Base64,
None, None,
None, None,

View File

@ -361,7 +361,7 @@ mod tests {
let nonce_pubkey = Pubkey::new(&[4u8; 32]); let nonce_pubkey = Pubkey::new(&[4u8; 32]);
let rpc_nonce_account = UiAccount::encode( let rpc_nonce_account = UiAccount::encode(
&nonce_pubkey, &nonce_pubkey,
nonce_account, &nonce_account,
UiAccountEncoding::Base64, UiAccountEncoding::Base64,
None, None,
None, None,

View File

@ -23,6 +23,13 @@ pub struct RpcSendTransactionConfig {
pub encoding: Option<UiTransactionEncoding>, pub encoding: Option<UiTransactionEncoding>,
} }
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcSimulateTransactionAccountsConfig {
pub encoding: Option<UiAccountEncoding>,
pub addresses: Vec<String>,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RpcSimulateTransactionConfig { pub struct RpcSimulateTransactionConfig {
@ -33,6 +40,7 @@ pub struct RpcSimulateTransactionConfig {
#[serde(flatten)] #[serde(flatten)]
pub commitment: Option<CommitmentConfig>, pub commitment: Option<CommitmentConfig>,
pub encoding: Option<UiTransactionEncoding>, pub encoding: Option<UiTransactionEncoding>,
pub accounts: Option<RpcSimulateTransactionAccountsConfig>,
} }
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]

View File

@ -318,6 +318,7 @@ pub struct RpcSignatureConfirmation {
pub struct RpcSimulateTransactionResult { pub struct RpcSimulateTransactionResult {
pub err: Option<TransactionError>, pub err: Option<TransactionError>,
pub logs: Option<Vec<String>>, pub logs: Option<Vec<String>>,
pub accounts: Option<Vec<Option<UiAccount>>>,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]

View File

@ -341,7 +341,7 @@ impl JsonRpcRequestProcessor {
for pubkey in pubkeys { for pubkey in pubkeys {
let response_account = let response_account =
get_encoded_account(&bank, &pubkey, encoding.clone(), config.data_slice)?; get_encoded_account(&bank, &pubkey, encoding, config.data_slice)?;
accounts.push(response_account) accounts.push(response_account)
} }
Ok(new_response(&bank, accounts)) Ok(new_response(&bank, accounts))
@ -387,8 +387,8 @@ impl JsonRpcRequestProcessor {
pubkey: pubkey.to_string(), pubkey: pubkey.to_string(),
account: UiAccount::encode( account: UiAccount::encode(
&pubkey, &pubkey,
account, &account,
encoding.clone(), encoding,
None, None,
data_slice_config, data_slice_config,
), ),
@ -1647,8 +1647,8 @@ impl JsonRpcRequestProcessor {
pubkey: pubkey.to_string(), pubkey: pubkey.to_string(),
account: UiAccount::encode( account: UiAccount::encode(
&pubkey, &pubkey,
account, &account,
encoding.clone(), encoding,
None, None,
data_slice_config, data_slice_config,
), ),
@ -1706,8 +1706,8 @@ impl JsonRpcRequestProcessor {
pubkey: pubkey.to_string(), pubkey: pubkey.to_string(),
account: UiAccount::encode( account: UiAccount::encode(
&pubkey, &pubkey,
account, &account,
encoding.clone(), encoding,
None, None,
data_slice_config, data_slice_config,
), ),
@ -1957,7 +1957,7 @@ fn get_encoded_account(
}); });
} else { } else {
response = Some(UiAccount::encode( response = Some(UiAccount::encode(
pubkey, account, encoding, None, data_slice, pubkey, &account, encoding, None, data_slice,
)); ));
} }
} }
@ -2970,12 +2970,13 @@ pub mod rpc_full {
} }
} }
if let (Err(err), logs) = preflight_bank.simulate_transaction(transaction.clone()) { if let (Err(err), logs, _) = preflight_bank.simulate_transaction(&transaction) {
return Err(RpcCustomError::SendTransactionPreflightFailure { return Err(RpcCustomError::SendTransactionPreflightFailure {
message: format!("Transaction simulation failed: {}", err), message: format!("Transaction simulation failed: {}", err),
result: RpcSimulateTransactionResult { result: RpcSimulateTransactionResult {
err: Some(err), err: Some(err),
logs: Some(logs), logs: Some(logs),
accounts: None,
}, },
} }
.into()); .into());
@ -3013,18 +3014,59 @@ pub mod rpc_full {
return Err(e); return Err(e);
} }
} }
let bank = &*meta.bank(config.commitment); let bank = &*meta.bank(config.commitment);
if config.replace_recent_blockhash { if config.replace_recent_blockhash {
transaction.message.recent_blockhash = bank.last_blockhash(); transaction.message.recent_blockhash = bank.last_blockhash();
} }
let (result, logs) = bank.simulate_transaction(transaction); let (result, logs, post_simulation_accounts) = bank.simulate_transaction(&transaction);
let accounts = if let Some(config_accounts) = config.accounts {
let accounts_encoding = config_accounts
.encoding
.unwrap_or(UiAccountEncoding::Base64);
if accounts_encoding == UiAccountEncoding::Binary
|| accounts_encoding == UiAccountEncoding::Base58
{
return Err(Error::invalid_params("base58 encoding not supported"));
}
if config_accounts.addresses.len() > post_simulation_accounts.len() {
return Err(Error::invalid_params(format!(
"Too many accounts provided; max {}",
post_simulation_accounts.len()
)));
}
let mut accounts = vec![];
for address_str in config_accounts.addresses {
let address = verify_pubkey(&address_str)?;
accounts.push(if result.is_err() {
None
} else {
transaction
.message
.account_keys
.iter()
.position(|pubkey| *pubkey == address)
.map(|i| post_simulation_accounts.get(i))
.flatten()
.map(|account| {
UiAccount::encode(&address, account, accounts_encoding, None, None)
})
});
}
Some(accounts)
} else {
None
};
Ok(new_response( Ok(new_response(
&bank, &bank,
RpcSimulateTransactionResult { RpcSimulateTransactionResult {
err: result.err(), err: result.err(),
logs: Some(logs), logs: Some(logs),
accounts,
}, },
)) ))
} }
@ -4928,7 +4970,6 @@ pub mod tests {
#[test] #[test]
fn test_rpc_simulate_transaction() { fn test_rpc_simulate_transaction() {
let bob_pubkey = solana_sdk::pubkey::new_rand();
let RpcHandler { let RpcHandler {
io, io,
meta, meta,
@ -4936,8 +4977,9 @@ pub mod tests {
alice, alice,
bank, bank,
.. ..
} = start_rpc_handler_with_tx(&bob_pubkey); } = start_rpc_handler_with_tx(&solana_sdk::pubkey::new_rand());
let bob_pubkey = solana_sdk::pubkey::new_rand();
let mut tx = system_transaction::transfer(&alice, &bob_pubkey, 1234, blockhash); let mut tx = system_transaction::transfer(&alice, &bob_pubkey, 1234, blockhash);
let tx_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string(); let tx_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string();
tx.signatures[0] = Signature::default(); tx.signatures[0] = Signature::default();
@ -4949,18 +4991,46 @@ pub mod tests {
// Good signature with sigVerify=true // Good signature with sigVerify=true
let req = format!( let req = format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#, r#"{{"jsonrpc":"2.0",
"id":1,
"method":"simulateTransaction",
"params":[
"{}",
{{
"sigVerify": true,
"accounts": {{
"encoding": "jsonParsed",
"addresses": ["{}", "{}"]
}}
}}
]
}}"#,
tx_serialized_encoded, tx_serialized_encoded,
solana_sdk::pubkey::new_rand(),
bob_pubkey,
); );
let res = io.handle_request_sync(&req, meta.clone()); let res = io.handle_request_sync(&req, meta.clone());
let expected = json!({ let expected = json!({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"result": { "result": {
"context":{"slot":0}, "context":{"slot":0},
"value":{"err":null, "logs":[ "value":{
"Program 11111111111111111111111111111111 invoke [1]", "accounts": [
"Program 11111111111111111111111111111111 success" null,
]} {
"data": ["", "base64"],
"executable": false,
"owner": "11111111111111111111111111111111",
"lamports": 1234,
"rentEpoch": 0
}
],
"err":null,
"logs":[
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
]
}
}, },
"id": 1, "id": 1,
}); });
@ -4970,6 +5040,43 @@ pub mod tests {
.expect("actual response deserialization"); .expect("actual response deserialization");
assert_eq!(expected, result); assert_eq!(expected, result);
// Too many input accounts...
let req = format!(
r#"{{"jsonrpc":"2.0",
"id":1,
"method":"simulateTransaction",
"params":[
"{}",
{{
"sigVerify": true,
"accounts": {{
"addresses": [
"11111111111111111111111111111111",
"11111111111111111111111111111111",
"11111111111111111111111111111111",
"11111111111111111111111111111111"
]
}}
}}
]
}}"#,
tx_serialized_encoded,
);
let res = io.handle_request_sync(&req, meta.clone());
let expected = json!({
"jsonrpc":"2.0",
"error": {
"code": error::ErrorCode::InvalidParams.code(),
"message": "Too many accounts provided; max 3"
},
"id":1
});
let expected: Response =
serde_json::from_value(expected).expect("expected response deserialization");
let result: Response = serde_json::from_str(&res.expect("actual response"))
.expect("actual response deserialization");
assert_eq!(expected, result);
// Bad signature with sigVerify=true // Bad signature with sigVerify=true
let req = format!( let req = format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#, r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#,
@ -5001,7 +5108,7 @@ pub mod tests {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"result": { "result": {
"context":{"slot":0}, "context":{"slot":0},
"value":{"err":null, "logs":[ "value":{"accounts": null, "err":null, "logs":[
"Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success" "Program 11111111111111111111111111111111 success"
]} ]}
@ -5024,7 +5131,7 @@ pub mod tests {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"result": { "result": {
"context":{"slot":0}, "context":{"slot":0},
"value":{"err":null, "logs":[ "value":{"accounts": null, "err":null, "logs":[
"Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success" "Program 11111111111111111111111111111111 success"
]} ]}
@ -5072,7 +5179,7 @@ pub mod tests {
"jsonrpc":"2.0", "jsonrpc":"2.0",
"result": { "result": {
"context":{"slot":0}, "context":{"slot":0},
"value":{"err": "BlockhashNotFound", "logs":[]} "value":{"err": "BlockhashNotFound", "accounts": null, "logs":[]}
}, },
"id":1 "id":1
}); });
@ -5093,7 +5200,7 @@ pub mod tests {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"result": { "result": {
"context":{"slot":0}, "context":{"slot":0},
"value":{"err":null, "logs":[ "value":{"accounts": null, "err":null, "logs":[
"Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success" "Program 11111111111111111111111111111111 success"
]} ]}
@ -5437,7 +5544,7 @@ pub mod tests {
assert_eq!( assert_eq!(
res, res,
Some( Some(
r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Blockhash not found","data":{"err":"BlockhashNotFound","logs":[]}},"id":1}"#.to_string(), r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Blockhash not found","data":{"accounts":null,"err":"BlockhashNotFound","logs":[]}},"id":1}"#.to_string(),
) )
); );

View File

@ -3233,12 +3233,16 @@ Simulate sending a transaction
#### Parameters: #### Parameters:
- `<string>` - Transaction, as an encoded string. The transaction must have a valid blockhash, but is not required to be signed. - `<string>` - Transaction, as an encoded string. The transaction must have a valid blockhash, but is not required to be signed.
- `<object>` - (optional) Configuration object containing the following field: - `<object>` - (optional) Configuration object containing the following fields:
- `sigVerify: <bool>` - if true the transaction signatures will be verified (default: false, conflicts with `replaceRecentBlockhash`) - `sigVerify: <bool>` - if true the transaction signatures will be verified (default: false, conflicts with `replaceRecentBlockhash`)
- `commitment: <string>` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to simulate the transaction at (default: `"finalized"`). - `commitment: <string>` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to simulate the transaction at (default: `"finalized"`).
- `encoding: <string>` - (optional) Encoding used for the transaction data. Either `"base58"` (*slow*, **DEPRECATED**), or `"base64"`. (default: `"base58"`). - `encoding: <string>` - (optional) Encoding used for the transaction data. Either `"base58"` (*slow*, **DEPRECATED**), or `"base64"`. (default: `"base58"`).
- `replaceRecentBlockhash: <bool>` - (optional) if true the transaction recent blockhash will be replaced with the most recent blockhash. - `replaceRecentBlockhash: <bool>` - (optional) if true the transaction recent blockhash will be replaced with the most recent blockhash.
(default: false, conflicts with `sigVerify`) (default: false, conflicts with `sigVerify`)
- `accounts: <object>` - (optional) Accounts configuration object containing the following fields:
- `encoding: <string>` - (optional) encoding for returned Account data, either "base64" (default), "base64+zstd" or "jsonParsed".
"jsonParsed" encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If "jsonParsed" is requested but a parser cannot be found, the field falls back to binary encoding, detectable when the `data` field is type `<string>`.
- `addresses: <array>` - An array of accounts to return, as base-58 encoded strings
#### Results: #### Results:
@ -3247,6 +3251,14 @@ The result will be an RpcResponse JSON object with `value` set to a JSON object
- `err: <object | string | null>` - Error if transaction failed, null if transaction succeeded. [TransactionError definitions](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction.rs#L24) - `err: <object | string | null>` - Error if transaction failed, null if transaction succeeded. [TransactionError definitions](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction.rs#L24)
- `logs: <array | null>` - Array of log messages the transaction instructions output during execution, null if simulation failed before the transaction was able to execute (for example due to an invalid blockhash or signature verification failure) - `logs: <array | null>` - Array of log messages the transaction instructions output during execution, null if simulation failed before the transaction was able to execute (for example due to an invalid blockhash or signature verification failure)
- `accounts: <array> | null>` - array of accounts with the same length as the `accounts.addresses` array in the request
- `<null>` - if the account doesn't exist or if `err` is not null
- `<object>` - otherwise, a JSON object containing:
- `lamports: <u64>`, number of lamports assigned to this account, as a u64
- `owner: <string>`, base-58 encoded Pubkey of the program this account has been assigned to
- `data: <[string, encoding]|object>`, data associated with the account, either as encoded binary data or JSON format `{<program>: <state>}`, depending on encoding parameter
- `executable: <bool>`, boolean indicating if the account contains a program \(and is strictly read-only\)
- `rentEpoch: <u64>`, the epoch at which this account will next owe rent, as u64
#### Example: #### Example:
@ -3273,6 +3285,7 @@ Result:
}, },
"value": { "value": {
"err": null, "err": null,
"accounts": null,
"logs": [ "logs": [
"BPF program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success" "BPF program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success"
] ]

View File

@ -28,7 +28,7 @@ pub fn get_parsed_token_account(
UiAccount::encode( UiAccount::encode(
pubkey, pubkey,
account, &account,
UiAccountEncoding::JsonParsed, UiAccountEncoding::JsonParsed,
additional_data, additional_data,
None, None,
@ -55,7 +55,7 @@ where
let maybe_encoded_account = UiAccount::encode( let maybe_encoded_account = UiAccount::encode(
&pubkey, &pubkey,
account, &account,
UiAccountEncoding::JsonParsed, UiAccountEncoding::JsonParsed,
additional_data, additional_data,
None, None,

View File

@ -296,7 +296,7 @@ fn filter_account_result(
Box::new(iter::once(get_parsed_token_account(bank, pubkey, account))) Box::new(iter::once(get_parsed_token_account(bank, pubkey, account)))
} else { } else {
Box::new(iter::once(UiAccount::encode( Box::new(iter::once(UiAccount::encode(
pubkey, account, encoding, None, None, pubkey, &account, encoding, None, None,
))) )))
} }
} else { } else {
@ -347,7 +347,7 @@ fn filter_program_results(
Box::new( Box::new(
keyed_accounts.map(move |(pubkey, account)| RpcKeyedAccount { keyed_accounts.map(move |(pubkey, account)| RpcKeyedAccount {
pubkey: pubkey.to_string(), pubkey: pubkey.to_string(),
account: UiAccount::encode(&pubkey, account, encoding.clone(), None, None), account: UiAccount::encode(&pubkey, &account, encoding, None, None),
}), }),
) )
}; };

View File

@ -2576,16 +2576,15 @@ impl Bank {
TransactionBatch::new(lock_results, &self, Cow::Borrowed(hashed_txs)) TransactionBatch::new(lock_results, &self, Cow::Borrowed(hashed_txs))
} }
pub fn prepare_simulation_batch<'a, 'b>( pub(crate) fn prepare_simulation_batch<'a, 'b>(
&'a self, &'a self,
txs: &'b [Transaction], tx: &'b Transaction,
) -> TransactionBatch<'a, 'b> { ) -> TransactionBatch<'a, 'b> {
let lock_results: Vec<_> = txs let mut batch = TransactionBatch::new(
.iter() vec![tx.sanitize().map_err(|e| e.into())],
.map(|tx| tx.sanitize().map_err(|e| e.into())) &self,
.collect(); Cow::Owned(vec![HashedTransaction::from(tx)]),
let hashed_txs = txs.iter().map(HashedTransaction::from).collect(); );
let mut batch = TransactionBatch::new(lock_results, &self, hashed_txs);
batch.needs_unlock = false; batch.needs_unlock = false;
batch batch
} }
@ -2593,17 +2592,16 @@ impl Bank {
/// Run transactions against a frozen bank without committing the results /// Run transactions against a frozen bank without committing the results
pub fn simulate_transaction( pub fn simulate_transaction(
&self, &self,
transaction: Transaction, transaction: &Transaction,
) -> (Result<()>, TransactionLogMessages) { ) -> (Result<()>, TransactionLogMessages, Vec<AccountSharedData>) {
assert!(self.is_frozen(), "simulation bank must be frozen"); assert!(self.is_frozen(), "simulation bank must be frozen");
let txs = &[transaction]; let batch = self.prepare_simulation_batch(&transaction);
let batch = self.prepare_simulation_batch(txs);
let mut timings = ExecuteTimings::default(); let mut timings = ExecuteTimings::default();
let ( let (
_loaded_accounts, loaded_accounts,
executed, executed,
_inner_instructions, _inner_instructions,
log_messages, log_messages,
@ -2625,10 +2623,18 @@ impl Bank {
let log_messages = log_messages let log_messages = log_messages
.get(0) .get(0)
.map_or(vec![], |messages| messages.to_vec()); .map_or(vec![], |messages| messages.to_vec());
let post_transaction_accounts = loaded_accounts
.into_iter()
.next()
.unwrap()
.0
.ok()
.map(|loaded_transaction| loaded_transaction.accounts.into_iter().collect::<Vec<_>>())
.unwrap_or_default();
debug!("simulate_transaction: {:?}", timings); debug!("simulate_transaction: {:?}", timings);
(transaction_result, log_messages) (transaction_result, log_messages, post_transaction_accounts)
} }
pub fn unlock_accounts(&self, batch: &mut TransactionBatch) { pub fn unlock_accounts(&self, batch: &mut TransactionBatch) {

View File

@ -83,7 +83,7 @@ mod tests {
let (bank, txs) = setup(); let (bank, txs) = setup();
// Prepare batch without locks // Prepare batch without locks
let batch = bank.prepare_simulation_batch(&txs); let batch = bank.prepare_simulation_batch(&txs[0]);
assert!(batch.lock_results().iter().all(|x| x.is_ok())); assert!(batch.lock_results().iter().all(|x| x.is_ok()));
// Grab locks // Grab locks
@ -91,7 +91,7 @@ mod tests {
assert!(batch2.lock_results().iter().all(|x| x.is_ok())); assert!(batch2.lock_results().iter().all(|x| x.is_ok()));
// Prepare another batch without locks // Prepare another batch without locks
let batch3 = bank.prepare_simulation_batch(&txs); let batch3 = bank.prepare_simulation_batch(&txs[0]);
assert!(batch3.lock_results().iter().all(|x| x.is_ok())); assert!(batch3.lock_results().iter().all(|x| x.is_ok()));
} }