use std::{
cell::RefCell,
collections::HashSet,
fmt::Debug,
hash::{Hash, Hasher},
iter::Iterator,
str::FromStr,
};
use crate::builder::SignerTrait;
use getset::{CopyGetters, Getters, MutGetters, Setters};
use hex_literal::hex;
use once_cell::sync::Lazy;
use primitive_types::H160;
use crate::neo_types::{Bytes, ContractParameter, InvocationResult, ScriptHash};
use crate::neo_builder::{
transaction::{
Signer, SignerType, Transaction, TransactionAttribute, TransactionError,
VerificationScript, Witness, WitnessScope,
},
BuilderError,
};
use crate::{
neo_clients::{APITrait, JsonRpcProvider, RpcClient},
neo_config::{NeoConstants, NEOCONFIG},
neo_crypto::{utils::ToHexString, Secp256r1PublicKey},
neo_protocol::AccountTrait,
};
use crate::neo_clients::public_key_to_script_hash;
use crate::neo_protocol::Account;
use super::init_logger;
#[derive(Getters, Setters, MutGetters, CopyGetters)]
pub struct TransactionBuilder<'a, P: JsonRpcProvider + 'static> {
pub(crate) client: Option<&'a RpcClient<P>>,
version: u8,
nonce: u32,
valid_until_block: Option<u32>,
#[getset(get = "pub")]
pub(crate) signers: Vec<Signer>,
#[getset(get = "pub", set = "pub")]
additional_network_fee: u64,
#[getset(get = "pub", set = "pub")]
additional_system_fee: u64,
#[getset(get = "pub")]
attributes: Vec<TransactionAttribute>,
#[getset(get = "pub", set = "pub")]
script: Option<Bytes>,
fee_consumer: Option<Box<dyn Fn(i64, i64)>>,
fee_error: Option<TransactionError>,
allows_transmission_on_fault: Option<bool>,
}
impl<'a, P: JsonRpcProvider + 'static> Debug for TransactionBuilder<'a, P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TransactionBuilder")
.field("version", &self.version)
.field("nonce", &self.nonce)
.field("valid_until_block", &self.valid_until_block)
.field("signers", &self.signers)
.field("additional_network_fee", &self.additional_network_fee)
.field("additional_system_fee", &self.additional_system_fee)
.field("attributes", &self.attributes)
.field("script", &self.script)
.field("fee_error", &self.fee_error)
.field("allows_transmission_on_fault", &self.allows_transmission_on_fault)
.finish()
}
}
impl<'a, P: JsonRpcProvider + 'static> Clone for TransactionBuilder<'a, P> {
fn clone(&self) -> Self {
Self {
client: self.client,
version: self.version,
nonce: self.nonce,
valid_until_block: self.valid_until_block,
signers: self.signers.clone(),
additional_network_fee: self.additional_network_fee,
additional_system_fee: self.additional_system_fee,
attributes: self.attributes.clone(),
script: self.script.clone(),
fee_consumer: None,
fee_error: None,
allows_transmission_on_fault: self.allows_transmission_on_fault,
}
}
}
impl<'a, P: JsonRpcProvider + 'static> Eq for TransactionBuilder<'a, P> {}
impl<'a, P: JsonRpcProvider + 'static> PartialEq for TransactionBuilder<'a, P> {
fn eq(&self, other: &Self) -> bool {
self.version == other.version
&& self.nonce == other.nonce
&& self.valid_until_block == other.valid_until_block
&& self.signers == other.signers
&& self.additional_network_fee == other.additional_network_fee
&& self.additional_system_fee == other.additional_system_fee
&& self.attributes == other.attributes
&& self.script == other.script
&& self.allows_transmission_on_fault == other.allows_transmission_on_fault
}
}
impl<'a, P: JsonRpcProvider + 'static> Hash for TransactionBuilder<'a, P> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.version.hash(state);
self.nonce.hash(state);
self.valid_until_block.hash(state);
self.signers.hash(state);
self.additional_network_fee.hash(state);
self.additional_system_fee.hash(state);
self.attributes.hash(state);
self.script.hash(state);
self.allows_transmission_on_fault.hash(state);
}
}
impl<'a, P: JsonRpcProvider + 'static> Default for TransactionBuilder<'a, P> {
fn default() -> Self {
Self::new()
}
}
pub static GAS_TOKEN_HASH: Lazy<ScriptHash> = Lazy::new(|| {
ScriptHash::from(hex!("d2a4cff31913016155e38e474a2c06d08be276cf"))
});
impl<'a, P: JsonRpcProvider + 'static> TransactionBuilder<'a, P> {
pub const BALANCE_OF_FUNCTION: &'static str = "balanceOf";
pub const DUMMY_PUB_KEY: &'static str =
"02ec143f00b88524caf36a0121c2de09eef0519ddbe1c710a00f0e2663201ee4c0";
pub fn new() -> Self {
Self {
client: None,
version: 0,
nonce: rand::random::<u32>(),
valid_until_block: None,
signers: Vec::new(),
additional_network_fee: 0,
additional_system_fee: 0,
attributes: Vec::new(),
script: None,
fee_consumer: None,
fee_error: None,
allows_transmission_on_fault: None,
}
}
pub fn with_client(client: &'a RpcClient<P>) -> Self {
Self {
client: Some(client),
version: 0,
nonce: rand::random::<u32>(),
valid_until_block: None,
signers: Vec::new(),
additional_network_fee: 0,
additional_system_fee: 0,
attributes: Vec::new(),
script: None,
fee_consumer: None,
fee_error: None,
allows_transmission_on_fault: None,
}
}
pub fn allow_transmission_on_fault(&mut self) -> &mut Self {
self.allows_transmission_on_fault = Some(true);
self
}
pub fn disallow_transmission_on_fault(&mut self) -> &mut Self {
self.allows_transmission_on_fault = Some(false);
self
}
pub fn version(&mut self, version: u8) -> &mut Self {
self.version = version;
self
}
pub fn nonce(&mut self, nonce: u32) -> Result<&mut Self, TransactionError> {
self.nonce = nonce;
Ok(self)
}
pub fn valid_until_block(&mut self, block: u32) -> Result<&mut Self, TransactionError> {
if block == 0 {
return Err(TransactionError::InvalidBlock);
}
self.valid_until_block = Some(block);
Ok(self)
}
pub fn first_signer(&mut self, sender: &Account) -> Result<&mut Self, TransactionError> {
self.first_signer_by_hash(&sender.get_script_hash())
}
pub fn first_signer_by_hash(&mut self, sender: &H160) -> Result<&mut Self, TransactionError> {
if self.signers.iter().any(|s| s.get_scopes().contains(&WitnessScope::None)) {
return Err(TransactionError::ScriptFormat("This transaction contains a signer with fee-only witness scope that will cover the fees. Hence, the order of the signers does not affect the payment of the fees.".to_string()));
}
if let Some(pos) = self.signers.iter().position(|s| s.get_signer_hash() == sender) {
let s = self.signers.remove(pos);
self.signers.insert(0, s);
Ok(self)
} else {
Err(TransactionError::ScriptFormat(format!("Could not find a signer with script hash {}. Make sure to add the signer before calling this method.", sender)))
}
}
pub fn extend_script(&mut self, script: Vec<u8>) -> &mut Self {
if let Some(ref mut existing_script) = self.script {
existing_script.extend(script);
} else {
self.script = Some(script);
}
self
}
pub async fn call_invoke_script(&self) -> Result<InvocationResult, TransactionError> {
let script = self.script.as_ref().ok_or(TransactionError::NoScript)?;
if script.is_empty() {
return Err(TransactionError::EmptyScript);
}
let client = self
.client
.ok_or_else(|| TransactionError::IllegalState("Client is not set".to_string()))?;
let result = client
.rpc_client()
.invoke_script(script.to_hex_string(), self.signers.clone())
.await
.map_err(TransactionError::ProviderError)?;
Ok(result)
}
pub async fn build(&mut self) -> Result<Transaction<'_, P>, TransactionError> {
self.get_unsigned_tx().await
}
pub fn validate(&self) -> Result<(), TransactionError> {
if self.signers.is_empty() {
return Err(TransactionError::NoSigners);
}
let mut seen_signers = std::collections::HashSet::new();
for signer in &self.signers {
let signer_hash = signer.get_signer_hash();
if !seen_signers.insert(signer_hash) {
return Err(TransactionError::DuplicateSigner);
}
}
if self.signers.len() > NeoConstants::MAX_SIGNER_SUBITEMS as usize {
return Err(TransactionError::TooManySigners);
}
match &self.script {
None => return Err(TransactionError::NoScript),
Some(script) if script.is_empty() => return Err(TransactionError::EmptyScript),
Some(_) => {},
}
if self.client.is_none() {
return Err(TransactionError::IllegalState(
"Client is not set. Use with_client() to set an RPC client.".to_string(),
));
}
Ok(())
}
pub fn is_ready(&self) -> bool {
self.validate().is_ok()
}
pub async fn get_unsigned_tx(&mut self) -> Result<Transaction<'_, P>, TransactionError> {
self.validate()?;
let mut seen = Vec::with_capacity(self.signers.len());
self.signers.retain(|s| {
if seen.iter().any(|prev| prev == s) {
false
} else {
seen.push(s.clone());
true
}
});
let client = self.client.expect("Client validated in validate()");
if self.valid_until_block.is_none() {
self.valid_until_block = Some(
self.fetch_current_block_count().await? + client.max_valid_until_block_increment()
- 1,
)
}
if self.is_high_priority() && !self.is_allowed_for_high_priority().await {
return Err(TransactionError::IllegalState("This transaction does not have a committee member as signer. Only committee members can send transactions with high priority.".to_string()));
}
let system_fee = self.get_system_fee().await? + self.additional_system_fee as i64;
let network_fee = self.get_network_fee().await? + self.additional_network_fee as i64;
let tx = Transaction {
network: Some(client),
version: self.version,
nonce: self.nonce,
valid_until_block: self.valid_until_block.unwrap_or(100),
size: 0,
sys_fee: system_fee,
net_fee: network_fee,
signers: self.signers.clone(),
attributes: self.attributes.clone(),
script: self
.script
.clone()
.ok_or_else(|| TransactionError::IllegalState("Script is not set".to_string()))?,
witnesses: vec![],
block_count_when_sent: None,
};
if self.fee_error.is_some()
&& !self.can_send_cover_fees(system_fee as u64 + network_fee as u64).await?
{
if let Some(supplier) = &self.fee_error {
return Err(supplier.clone());
}
} else if let Some(fee_consumer) = &self.fee_consumer {
let sender_balance_u64 = self.get_sender_balance().await?;
let sender_balance = i64::try_from(sender_balance_u64).unwrap_or_else(|_| {
tracing::warn!(
balance = sender_balance_u64,
"Sender balance exceeds i64::MAX; saturating for fee checks"
);
i64::MAX
});
let total_fee = network_fee + system_fee;
if total_fee > sender_balance {
fee_consumer(total_fee, sender_balance);
}
}
Ok(tx)
}
async fn get_system_fee(&self) -> Result<i64, TransactionError> {
let script = self.script.as_ref().ok_or_else(|| TransactionError::NoScript)?;
let client = self
.client
.ok_or_else(|| TransactionError::IllegalState("Client is not set".to_string()))?;
let response = client
.invoke_script(script.to_hex_string(), vec![self.signers[0].clone()])
.await
.map_err(TransactionError::ProviderError)?;
if response.has_state_fault() {
let allows_fault = match self.allows_transmission_on_fault {
Some(allows_fault) => allows_fault,
None => {
NEOCONFIG
.lock()
.map_err(|_| {
TransactionError::IllegalState("Failed to lock NEOCONFIG".to_string())
})?
.allows_transmission_on_fault
},
};
if !allows_fault {
return Err(TransactionError::TransactionConfiguration(format!(
"The vm exited due to the following exception: {}",
response.exception.unwrap_or_else(|| "Unknown exception".to_string())
)));
}
}
i64::from_str(&response.gas_consumed)
.map_err(|_| TransactionError::IllegalState("Failed to parse gas consumed".to_string()))
}
async fn get_network_fee(&mut self) -> Result<i64, TransactionError> {
let client = self
.client
.ok_or_else(|| TransactionError::IllegalState("Client is not set".to_string()))?;
let script = self.script.clone().unwrap_or_default();
let valid_until_block = self.valid_until_block.unwrap_or(100);
let mut tx = Transaction {
network: Some(client),
version: self.version,
nonce: self.nonce,
valid_until_block,
size: 0,
sys_fee: 0,
net_fee: 0,
signers: self.signers.clone(),
attributes: self.attributes.clone(),
script,
witnesses: vec![],
block_count_when_sent: None,
};
let mut has_atleast_one_signing_account = false;
for signer in self.signers.iter() {
match signer {
Signer::ContractSigner(contract_signer) => {
let witness =
Witness::create_contract_witness(contract_signer.verify_params().to_vec())
.map_err(|e| {
TransactionError::IllegalState(format!(
"Failed to create contract witness: {}",
e
))
})?;
tx.add_witness(witness);
},
Signer::AccountSigner(account_signer) => {
let account = account_signer.account();
let verification_script = if let Some(vs) = account.verification_script() {
vs.clone()
} else if account.is_multi_sig() {
self.create_placeholder_multi_sig_verification_script(account).map_err(
|e| {
TransactionError::IllegalState(format!(
"Failed to create multi-sig verification script: {}",
e
))
},
)?
} else {
if let Some(key_pair) = account.key_pair() {
VerificationScript::from_public_key(&key_pair.public_key)
} else {
self.create_placeholder_single_sig_verification_script().map_err(|e| {
TransactionError::IllegalState(format!(
"Failed to create single-sig verification script: {}",
e
))
})?
}
};
tx.add_witness(Witness::from_scripts(
vec![],
verification_script.script().to_vec(),
));
has_atleast_one_signing_account = true;
},
_ => {
},
}
}
if !has_atleast_one_signing_account {
return Err(TransactionError::TransactionConfiguration("A transaction requires at least one signing account (i.e. an AccountSigner). None was provided.".to_string()));
}
let tx_hex = tx.try_to_array().map(|bytes| bytes.to_hex_string()).map_err(|err| {
TransactionError::TransactionConfiguration(format!(
"Failed to serialize transaction for network fee calculation: {}",
err
))
})?;
let fee = client.calculate_network_fee(tx_hex).await?;
Ok(fee.network_fee)
}
async fn fetch_current_block_count(&mut self) -> Result<u32, TransactionError> {
let client = self
.client
.ok_or_else(|| TransactionError::IllegalState("Client is not set".to_string()))?;
let count = client.get_block_count().await?;
Ok(count)
}
async fn get_sender_balance(&self) -> Result<u64, TransactionError> {
let sender = &self.signers[0];
if Self::is_account_signer(sender) {
let client = self
.client
.ok_or_else(|| TransactionError::IllegalState("Client is not set".to_string()))?;
let balance = client
.invoke_function(
&GAS_TOKEN_HASH,
Self::BALANCE_OF_FUNCTION.to_string(),
vec![ContractParameter::from(sender.get_signer_hash())],
None,
)
.await
.map_err(TransactionError::ProviderError)?
.stack[0]
.clone();
return Ok(balance.as_int().ok_or_else(|| {
TransactionError::IllegalState("Failed to parse balance as integer".to_string())
})? as u64);
}
Err(TransactionError::InvalidSender)
}
fn create_placeholder_single_sig_verification_script(
&self,
) -> Result<VerificationScript, TransactionError> {
let placeholder_public_key = Secp256r1PublicKey::from_encoded(Self::DUMMY_PUB_KEY)
.ok_or_else(|| {
TransactionError::IllegalState(
"Failed to create placeholder public key".to_string(),
)
})?;
Ok(VerificationScript::from_public_key(&placeholder_public_key))
}
fn create_placeholder_multi_sig_verification_script(
&self,
account: &Account,
) -> Result<VerificationScript, TransactionError> {
let nr_of_participants = account.get_nr_of_participants().map_err(|e| {
TransactionError::IllegalState(format!("Failed to get number of participants: {}", e))
})?;
let mut pub_keys: Vec<Secp256r1PublicKey> = Vec::with_capacity(nr_of_participants as usize);
for _ in 0..nr_of_participants {
let placeholder_public_key = Secp256r1PublicKey::from_encoded(Self::DUMMY_PUB_KEY)
.ok_or_else(|| {
TransactionError::IllegalState(
"Failed to create placeholder public key".to_string(),
)
})?;
pub_keys.push(placeholder_public_key);
}
let threshold_value = account.get_signing_threshold().map_err(|e| {
TransactionError::IllegalState(format!("Failed to get signing threshold: {}", e))
})?;
let signing_threshold = u8::try_from(threshold_value).map_err(|_| {
TransactionError::IllegalState(
"Signing threshold value out of range for u8".to_string(),
)
})?;
let script = VerificationScript::from_multi_sig(&mut pub_keys[..], signing_threshold);
Ok(script)
}
fn is_account_signer(signer: &Signer) -> bool {
signer.get_type() == SignerType::AccountSigner
}
pub async fn sign(&mut self) -> Result<Transaction<'_, P>, BuilderError> {
init_logger();
let mut unsigned_tx = self.get_unsigned_tx().await?;
let tx_bytes = unsigned_tx.get_hash_data().await?;
let mut witnesses_to_add = Vec::with_capacity(unsigned_tx.signers.len());
for signer in &mut unsigned_tx.signers {
if Self::is_account_signer(signer) {
let account_signer = signer.as_account_signer().ok_or_else(|| {
BuilderError::IllegalState("Failed to get account signer".to_string())
})?;
let acc = &account_signer.account;
if acc.is_multi_sig() {
return Err(BuilderError::IllegalState(
"Transactions with multi-sig signers cannot be signed automatically."
.to_string(),
));
}
let key_pair = acc.key_pair().as_ref().ok_or_else(|| {
BuilderError::InvalidConfiguration(
format!("Cannot create transaction signature because account {} does not hold a private key.", acc.get_address()),
)
})?;
witnesses_to_add.push(Witness::create(tx_bytes.clone(), key_pair)?);
} else {
let contract_signer = signer.as_contract_signer().ok_or_else(|| {
BuilderError::IllegalState(
"Expected contract signer but found another type".to_string(),
)
})?;
witnesses_to_add.push(Witness::create_contract_witness(
contract_signer.verify_params().clone(),
)?);
}
}
for witness in witnesses_to_add {
unsigned_tx.add_witness(witness);
}
Ok(unsigned_tx)
}
fn signers_contain_multi_sig_with_committee_member(&self, committee: &HashSet<H160>) -> bool {
for signer in &self.signers {
if let Some(account_signer) = signer.as_account_signer() {
if account_signer.is_multi_sig() {
if let Some(script) = &account_signer.account().verification_script() {
if let Ok(public_keys) = script.get_public_keys() {
for pubkey in public_keys {
let hash = public_key_to_script_hash(&pubkey);
if committee.contains(&hash) {
return true;
}
}
}
}
}
}
}
false
}
pub fn set_signers(&mut self, signers: Vec<Signer>) -> Result<&mut Self, TransactionError> {
if self.contains_duplicate_signers(&signers) {
return Err(TransactionError::TransactionConfiguration(
"Cannot add multiple signers concerning the same account.".to_string(),
));
}
self.check_and_throw_if_max_attributes_exceeded(signers.len(), self.attributes.len())?;
self.signers = signers;
Ok(self)
}
pub fn add_attributes(
&mut self,
attributes: Vec<TransactionAttribute>,
) -> Result<&mut Self, TransactionError> {
self.check_and_throw_if_max_attributes_exceeded(
self.signers.len(),
self.attributes.len() + attributes.len(),
)?;
for attr in attributes {
match attr {
TransactionAttribute::HighPriority => {
self.add_high_priority_attribute(attr)?;
},
TransactionAttribute::NotValidBefore { height: _ } => {
self.add_not_valid_before_attribute(attr)?;
},
TransactionAttribute::Conflicts { hash: _ } => {
self.add_conflicts_attribute(attr)?;
},
_ => {
self.attributes.push(attr);
},
}
}
Ok(self)
}
fn add_high_priority_attribute(
&mut self,
attr: TransactionAttribute,
) -> Result<(), TransactionError> {
if self.is_high_priority() {
return Err(TransactionError::TransactionConfiguration(
"A transaction can only have one HighPriority attribute.".to_string(),
));
}
self.attributes.push(attr);
Ok(())
}
fn add_not_valid_before_attribute(
&mut self,
attr: TransactionAttribute,
) -> Result<(), TransactionError> {
if self.has_attribute_of_type(TransactionAttribute::NotValidBefore { height: 0 }) {
return Err(TransactionError::TransactionConfiguration(
"A transaction can only have one NotValidBefore attribute.".to_string(),
));
}
self.attributes.push(attr);
Ok(())
}
fn add_conflicts_attribute(
&mut self,
attr: TransactionAttribute,
) -> Result<(), TransactionError> {
if self.has_attribute(&attr) {
let hash = attr.get_hash().ok_or_else(|| {
TransactionError::IllegalState(
"Expected Conflicts attribute to have a hash".to_string(),
)
})?;
return Err(TransactionError::TransactionConfiguration(format!(
"There already exists a conflicts attribute for the hash {} in this transaction.",
hash
)));
}
self.attributes.push(attr);
Ok(())
}
fn has_attribute_of_type(&self, attr_type: TransactionAttribute) -> bool {
self.attributes.iter().any(|attr| {
matches!(
(attr, &attr_type),
(
TransactionAttribute::NotValidBefore { .. },
TransactionAttribute::NotValidBefore { .. },
) | (TransactionAttribute::HighPriority, TransactionAttribute::HighPriority)
)
})
}
fn has_attribute(&self, attr: &TransactionAttribute) -> bool {
self.attributes.iter().any(|a| a == attr)
}
fn is_high_priority(&self) -> bool {
self.has_attribute_of_type(TransactionAttribute::HighPriority)
}
fn contains_duplicate_signers(&self, signers: &[Signer]) -> bool {
let signer_list: Vec<H160> = signers.iter().map(|s| *s.get_signer_hash()).collect();
let signer_set: HashSet<_> = signer_list.iter().collect();
signer_list.len() != signer_set.len()
}
fn check_and_throw_if_max_attributes_exceeded(
&self,
total_signers: usize,
total_attributes: usize,
) -> Result<(), TransactionError> {
let max_attributes = NeoConstants::MAX_TRANSACTION_ATTRIBUTES.try_into().map_err(|e| {
TransactionError::IllegalState(format!(
"Failed to convert MAX_TRANSACTION_ATTRIBUTES to usize: {}",
e
))
})?;
if total_signers + total_attributes > max_attributes {
return Err(TransactionError::TransactionConfiguration(format!(
"A transaction cannot have more than {} attributes (including signers).",
NeoConstants::MAX_TRANSACTION_ATTRIBUTES
)));
}
Ok(())
}
async fn is_allowed_for_high_priority(&self) -> bool {
let client = match self.client {
Some(client) => client,
None => return false, };
let response = match client.get_committee().await.map_err(TransactionError::ProviderError) {
Ok(response) => response,
Err(_) => return false, };
let committee: HashSet<H160> = response
.iter()
.filter_map(|key_str| {
let public_key = Secp256r1PublicKey::from_encoded(key_str)?;
Some(public_key_to_script_hash(&public_key)) })
.collect();
let signers_contain_committee_member = self
.signers
.iter()
.map(|signer| signer.get_signer_hash())
.any(|script_hash| committee.contains(script_hash));
if signers_contain_committee_member {
return true;
}
self.signers_contain_multi_sig_with_committee_member(&committee)
}
pub fn do_if_sender_cannot_cover_fees<F>(
&mut self,
consumer: F,
) -> Result<&mut Self, TransactionError>
where
F: FnMut(i64, i64) + Send + Sync + 'static,
{
if self.fee_error.is_some() {
return Err(TransactionError::IllegalState(
"Cannot handle a consumer for this case, since an exception will be thrown if the sender cannot cover the fees.".to_string(),
));
}
let consumer = RefCell::new(consumer);
self.fee_consumer = Some(Box::new(move |fee, balance| {
let mut consumer = consumer.borrow_mut();
consumer(fee, balance);
}));
Ok(self)
}
pub fn throw_if_sender_cannot_cover_fees(
&mut self,
error: TransactionError,
) -> Result<&mut Self, TransactionError> {
if self.fee_consumer.is_some() {
return Err(TransactionError::IllegalState(
"Cannot handle a supplier for this case, since a consumer will be executed if the sender cannot cover the fees.".to_string(),
));
}
self.fee_error = Some(error);
Ok(self)
}
async fn can_send_cover_fees(&self, fees: u64) -> Result<bool, BuilderError> {
let balance = self.get_sender_balance().await?;
Ok(balance >= fees)
}
}