use crate::errors::AuthError;
#[cfg(any(test, feature = "testing"))]
use crate::test_utils::divide_seed;
use crate::AuthFuture;
use crate::AuthMsgTx;
use futures::future;
use futures::Future;
use log::trace;
use lru_cache::LruCache;
use rand::rngs::StdRng;
use rand::{thread_rng, CryptoRng, Rng, SeedableRng};
use safe_core::client::account::Account;
use safe_core::client::{req, AuthActions, Inner, SafeKey, IMMUT_DATA_CACHE_SIZE};
use safe_core::config_handler::Config;
use safe_core::crypto::{shared_box, shared_secretbox};
use safe_core::fry;
use safe_core::ipc::BootstrapConfig;
use safe_core::{utils, Client, ClientKeys, ConnectionManager, CoreError, MDataInfo, NetworkTx};
use safe_nd::{
ClientFullId, LoginPacket, Message, MessageId, PublicId, PublicKey, Request, Response, XorName,
};
use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
use std::time::Duration;
use tiny_keccak::sha3_256;
use tokio::runtime::current_thread::{block_on_all, Handle};
use unwrap::unwrap;
pub struct AuthClient {
inner: Rc<RefCell<Inner<AuthClient, ()>>>,
auth_inner: Rc<RefCell<AuthInner>>,
}
impl AuthClient {
pub(crate) fn registered(
acc_locator: &str,
acc_password: &str,
client_id: ClientFullId,
el_handle: Handle,
core_tx: AuthMsgTx,
net_tx: NetworkTx,
) -> Result<Self, AuthError> {
Self::registered_impl(
acc_locator.as_bytes(),
acc_password.as_bytes(),
client_id,
el_handle,
core_tx,
net_tx,
None::<&mut StdRng>,
|cm| cm,
)
}
#[cfg(any(test, feature = "testing"))]
pub(crate) fn registered_with_seed(
seed: &str,
client_id: ClientFullId,
el_handle: Handle,
core_tx: AuthMsgTx,
net_tx: NetworkTx,
) -> Result<Self, AuthError> {
let arr = divide_seed(seed)?;
let seed = sha3_256(seed.as_bytes());
let mut rng = StdRng::from_seed(seed);
Self::registered_impl(
arr[0],
arr[1],
client_id,
el_handle,
core_tx,
net_tx,
Some(&mut rng),
|cm| cm,
)
}
#[cfg(all(feature = "mock-network", any(test, feature = "testing")))]
pub fn registered_with_hook<F>(
acc_locator: &str,
acc_password: &str,
client_id: ClientFullId,
el_handle: Handle,
core_tx: AuthMsgTx,
net_tx: NetworkTx,
connection_manager_wrapper_fn: F,
) -> Result<Self, AuthError>
where
F: Fn(ConnectionManager) -> ConnectionManager,
{
Self::registered_impl(
acc_locator.as_bytes(),
acc_password.as_bytes(),
client_id,
el_handle,
core_tx,
net_tx,
None::<&mut StdRng>,
connection_manager_wrapper_fn,
)
}
#[allow(clippy::too_many_arguments)]
fn registered_impl<F, R>(
acc_locator: &[u8],
acc_password: &[u8],
client_id: ClientFullId,
el_handle: Handle,
core_tx: AuthMsgTx,
net_tx: NetworkTx,
seed: Option<&mut R>,
connection_manager_wrapper_fn: F,
) -> Result<Self, AuthError>
where
R: CryptoRng + SeedableRng + Rng,
F: Fn(ConnectionManager) -> ConnectionManager,
{
trace!("Creating an account.");
let (password, keyword, pin) = utils::derive_secrets(acc_locator, acc_password);
let acc_locator = Account::generate_network_id(&keyword, &pin)?;
let user_cred = UserCred::new(password, pin);
let mut maid_keys = match seed {
Some(seed) => ClientKeys::new(seed),
None => ClientKeys::new(&mut thread_rng()),
};
maid_keys.client_id = client_id.clone();
let balance_full_id = SafeKey::client(client_id);
let client_safe_key = maid_keys.client_safe_key();
let acc = Account::new(maid_keys)?;
let acc_ciphertext = acc.encrypt(&user_cred.password, &user_cred.pin)?;
let transient_id = create_client_id(&acc_locator.0);
let sig = transient_id.sign(&acc_ciphertext);
let transient_pk = transient_id.public_id().public_key();
let new_login_packet = LoginPacket::new(acc_locator, *transient_pk, acc_ciphertext, sig)?;
let mut connection_manager =
ConnectionManager::new(Config::new().quic_p2p, &net_tx.clone())?;
connection_manager = connection_manager_wrapper_fn(connection_manager);
{
trace!("Using throw-away connection group to insert a login packet.");
block_on_all(connection_manager.bootstrap(balance_full_id.clone()))?;
let response = req(
&mut connection_manager,
Request::CreateLoginPacket(new_login_packet),
&balance_full_id,
)?;
match response {
Response::Mutation(res) => res?,
_ => return Err(AuthError::from("Unexpected response")),
};
block_on_all(connection_manager.disconnect(&balance_full_id.public_id()))?;
}
block_on_all(connection_manager.bootstrap(client_safe_key))?;
Ok(Self {
inner: Rc::new(RefCell::new(Inner::new(
el_handle,
connection_manager,
LruCache::new(IMMUT_DATA_CACHE_SIZE),
Duration::from_secs(180),
core_tx,
net_tx,
))),
auth_inner: Rc::new(RefCell::new(AuthInner {
acc,
acc_loc: acc_locator,
user_cred,
})),
})
}
pub(crate) fn login(
acc_locator: &str,
acc_password: &str,
el_handle: Handle,
core_tx: AuthMsgTx,
net_tx: NetworkTx,
) -> Result<Self, AuthError> {
Self::login_impl(
acc_locator.as_bytes(),
acc_password.as_bytes(),
el_handle,
core_tx,
net_tx,
|routing| routing,
)
}
#[cfg(any(test, feature = "testing"))]
pub(crate) fn login_with_seed(
seed: &str,
el_handle: Handle,
core_tx: AuthMsgTx,
net_tx: NetworkTx,
) -> Result<Self, AuthError> {
let arr = divide_seed(seed)?;
Self::login_impl(arr[0], arr[1], el_handle, core_tx, net_tx, |routing| {
routing
})
}
#[cfg(all(feature = "mock-network", any(test, feature = "testing")))]
pub fn login_with_hook<F>(
acc_locator: &str,
acc_password: &str,
el_handle: Handle,
core_tx: AuthMsgTx,
net_tx: NetworkTx,
connection_manager_wrapper_fn: F,
) -> Result<Self, AuthError>
where
F: Fn(ConnectionManager) -> ConnectionManager,
{
Self::login_impl(
acc_locator.as_bytes(),
acc_password.as_bytes(),
el_handle,
core_tx,
net_tx,
connection_manager_wrapper_fn,
)
}
fn login_impl<F>(
acc_locator: &[u8],
acc_password: &[u8],
el_handle: Handle,
core_tx: AuthMsgTx,
net_tx: NetworkTx,
connection_manager_wrapper_fn: F,
) -> Result<Self, AuthError>
where
F: Fn(ConnectionManager) -> ConnectionManager,
{
trace!("Attempting to log into an acc.");
let (password, keyword, pin) = utils::derive_secrets(acc_locator, acc_password);
let acc_locator = Account::generate_network_id(&keyword, &pin)?;
let client_full_id = create_client_id(&acc_locator.0);
let client_pk = *client_full_id.public_id().public_key();
let client_full_id = SafeKey::client(client_full_id);
let user_cred = UserCred::new(password, pin);
let mut connection_manager =
ConnectionManager::new(Config::new().quic_p2p, &net_tx.clone())?;
connection_manager = connection_manager_wrapper_fn(connection_manager);
let (account_buffer, signature) = {
trace!("Using throw-away connection group to get a login packet.");
block_on_all(connection_manager.bootstrap(client_full_id.clone()))?;
let response = req(
&mut connection_manager,
Request::GetLoginPacket(acc_locator),
&client_full_id,
)?;
block_on_all(connection_manager.disconnect(&client_full_id.public_id()))?;
match response {
Response::GetLoginPacket(res) => res?,
_ => return Err(AuthError::from("Unexpected response")),
}
};
client_pk.verify(&signature, account_buffer.as_slice())?;
let acc = Account::decrypt(
account_buffer.as_slice(),
&user_cred.password,
&user_cred.pin,
)?;
let id_packet = acc.maid_keys.client_safe_key();
trace!("Creating an actual client...");
block_on_all(connection_manager.bootstrap(id_packet))?;
Ok(Self {
inner: Rc::new(RefCell::new(Inner::new(
el_handle,
connection_manager,
LruCache::new(IMMUT_DATA_CACHE_SIZE),
Duration::from_secs(180),
core_tx,
net_tx,
))),
auth_inner: Rc::new(RefCell::new(AuthInner {
acc,
acc_loc: acc_locator,
user_cred,
})),
})
}
pub fn config_root_dir(&self) -> MDataInfo {
let auth_inner = self.auth_inner.borrow();
auth_inner.acc.config_root.clone()
}
pub fn set_config_root_dir(&self, dir: MDataInfo) -> bool {
trace!("Setting configuration root Dir ID.");
let mut auth_inner = self.auth_inner.borrow_mut();
let acc = &mut auth_inner.acc;
if acc.config_root == dir {
false
} else {
acc.config_root = dir;
true
}
}
pub fn access_container(&self) -> MDataInfo {
let auth_inner = self.auth_inner.borrow();
auth_inner.acc.access_container.clone()
}
pub fn set_access_container(&self, dir: MDataInfo) -> bool {
trace!("Setting user root Dir ID.");
let mut auth_inner = self.auth_inner.borrow_mut();
let account = &mut auth_inner.acc;
if account.access_container == dir {
false
} else {
account.access_container = dir;
true
}
}
fn prepare_account_packet_update(
acc_loc: XorName,
account: &Account,
keys: &UserCred,
full_id: &SafeKey,
) -> Result<LoginPacket, AuthError> {
let encrypted_account = account.encrypt(&keys.password, &keys.pin)?;
let sig = full_id.sign(&encrypted_account);
let client_pk = match full_id.public_id() {
PublicId::Client(id) => *id.public_key(),
x => panic!("Unexpected ID type {:?}", x),
};
LoginPacket::new(acc_loc, client_pk, encrypted_account, sig).map_err(AuthError::from)
}
pub fn update_account_packet(&self) -> Box<AuthFuture<()>> {
trace!("Updating account packet.");
let auth_inner = self.auth_inner.borrow();
let account = &auth_inner.acc;
let keys = &auth_inner.user_cred;
let acc_loc = &auth_inner.acc_loc;
let account_packet_id = SafeKey::client(create_client_id(&acc_loc.0));
let account_pub_id = account_packet_id.public_id();
let updated_packet = fry!(Self::prepare_account_packet_update(
*acc_loc,
account,
keys,
&account_packet_id
));
let mut client_inner = self.inner.borrow_mut();
let mut cm = client_inner.cm().clone();
let mut cm2 = cm.clone();
let mut cm4 = cm.clone();
let message_id = MessageId::new();
let request = Request::UpdateLoginPacket(updated_packet);
let signature =
account_packet_id.sign(&unwrap!(bincode::serialize(&(&request, message_id))));
let account_pub_id2 = account_pub_id.clone();
Box::new(
future::lazy(move || cm.bootstrap(account_packet_id))
.and_then(move |_| {
cm2.send(
&account_pub_id,
&Message::Request {
request,
message_id,
signature: Some(signature),
},
)
})
.and_then(move |resp| match resp {
Response::Mutation(res) => res.map_err(CoreError::from),
_ => Err(CoreError::from("Unexpected response")),
})
.and_then(move |_resp| cm4.disconnect(&account_pub_id2))
.map_err(AuthError::from),
)
}
pub fn std_dirs_created(&self) -> bool {
let auth_inner = self.auth_inner.borrow();
auth_inner.acc.root_dirs_created
}
pub fn set_std_dirs_created(&self, val: bool) {
let mut auth_inner = self.auth_inner.borrow_mut();
let account = &mut auth_inner.acc;
account.root_dirs_created = val;
}
}
fn create_client_id(seeder: &[u8]) -> ClientFullId {
let seed = sha3_256(&seeder);
let mut rng = StdRng::from_seed(seed);
ClientFullId::new_bls(&mut rng)
}
impl AuthActions for AuthClient {}
impl Client for AuthClient {
type Context = ();
fn full_id(&self) -> SafeKey {
let auth_inner = self.auth_inner.borrow();
auth_inner.acc.maid_keys.client_safe_key()
}
fn owner_key(&self) -> PublicKey {
self.public_key()
}
fn config(&self) -> Option<BootstrapConfig> {
None
}
fn inner(&self) -> Rc<RefCell<Inner<Self, Self::Context>>> {
self.inner.clone()
}
fn public_encryption_key(&self) -> threshold_crypto::PublicKey {
let auth_inner = self.auth_inner.borrow();
auth_inner.acc.maid_keys.enc_public_key
}
fn secret_encryption_key(&self) -> shared_box::SecretKey {
let auth_inner = self.auth_inner.borrow();
auth_inner.acc.maid_keys.enc_secret_key.clone()
}
fn secret_symmetric_key(&self) -> shared_secretbox::Key {
let auth_inner = self.auth_inner.borrow();
auth_inner.acc.maid_keys.enc_key.clone()
}
}
impl fmt::Debug for AuthClient {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Safe Authenticator Client")
}
}
impl Clone for AuthClient {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
auth_inner: Rc::clone(&self.auth_inner),
}
}
}
struct AuthInner {
acc: Account,
acc_loc: XorName,
user_cred: UserCred,
}
#[derive(Clone)]
struct UserCred {
pin: Vec<u8>,
password: Vec<u8>,
}
impl UserCred {
fn new(password: Vec<u8>, pin: Vec<u8>) -> Self {
Self { pin, password }
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::sync::mpsc;
use futures::Future;
use safe_core::client::test_create_balance;
use safe_core::ok;
use safe_core::utils::test_utils::{
calculate_new_balance, finish, gen_client_id, random_client, setup_client,
};
use safe_core::{utils, CoreError, DIR_TAG};
use safe_nd::{Coins, Error as SndError, MDataKind};
use std::str::FromStr;
use tokio::runtime::current_thread::Runtime;
use AuthMsgTx;
#[test]
fn registered_client() {
let el = unwrap!(Runtime::new());
let (core_tx, _): (AuthMsgTx, _) = mpsc::unbounded();
let (net_tx, _) = mpsc::unbounded();
let sec_0 = unwrap!(utils::generate_random_string(10));
let sec_1 = unwrap!(utils::generate_random_string(10));
let client_id = gen_client_id();
unwrap!(test_create_balance(
&client_id,
unwrap!(Coins::from_str("10"))
));
let _ = unwrap!(AuthClient::registered(
&sec_0,
&sec_1,
client_id.clone(),
el.handle(),
core_tx.clone(),
net_tx.clone(),
));
match AuthClient::registered(&sec_0, &sec_1, client_id, el.handle(), core_tx, net_tx) {
Ok(_) => panic!("Account name hijacking should fail"),
Err(AuthError::SndError(SndError::LoginPacketExists)) => (),
Err(err) => panic!("{:?}", err),
}
}
#[test]
fn login() {
let sec_0 = unwrap!(utils::generate_random_string(10));
let sec_1 = unwrap!(utils::generate_random_string(10));
let client_id = gen_client_id();
unwrap!(test_create_balance(
&client_id,
unwrap!(Coins::from_str("10"))
));
setup_client(
&(),
|el_h, core_tx, net_tx| {
match AuthClient::login(
&sec_0,
&sec_1,
el_h.clone(),
core_tx.clone(),
net_tx.clone(),
) {
Err(AuthError::SndError(SndError::NoSuchLoginPacket)) => (),
x => panic!("Unexpected Login outcome: {:?}", x),
}
AuthClient::registered(&sec_0, &sec_1, client_id, el_h, core_tx, net_tx)
},
|_| finish(),
);
setup_client(
&(),
|el_h, core_tx, net_tx| AuthClient::login(&sec_0, &sec_1, el_h, core_tx, net_tx),
|_| finish(),
);
}
#[test]
fn seeded_login() {
let invalid_seed = String::from("123");
{
let el = unwrap!(Runtime::new());
let (core_tx, _): (AuthMsgTx, _) = mpsc::unbounded();
let (net_tx, _) = mpsc::unbounded();
let client_id = gen_client_id();
match AuthClient::registered_with_seed(
&invalid_seed,
client_id,
el.handle(),
core_tx,
net_tx,
) {
Err(AuthError::Unexpected(_)) => (),
_ => panic!("Expected a failure"),
}
}
{
let el = unwrap!(Runtime::new());
let (core_tx, _): (AuthMsgTx, _) = mpsc::unbounded();
let (net_tx, _) = mpsc::unbounded();
match AuthClient::login_with_seed(&invalid_seed, el.handle(), core_tx, net_tx) {
Err(AuthError::Unexpected(_)) => (),
_ => panic!("Expected a failure"),
}
}
let seed = unwrap!(utils::generate_random_string(30));
let client_id = gen_client_id();
unwrap!(test_create_balance(
&client_id,
unwrap!(Coins::from_str("10"))
));
setup_client(
&(),
|el_h, core_tx, net_tx| {
match AuthClient::login_with_seed(
&seed,
el_h.clone(),
core_tx.clone(),
net_tx.clone(),
) {
Err(AuthError::SndError(SndError::NoSuchLoginPacket)) => (),
x => panic!("Unexpected Login outcome: {:?}", x),
}
AuthClient::registered_with_seed(&seed, client_id, el_h, core_tx, net_tx)
},
|_| finish(),
);
setup_client(
&(),
|el_h, core_tx, net_tx| AuthClient::login_with_seed(&seed, el_h, core_tx, net_tx),
|_| finish(),
);
}
#[test]
fn access_container_creation() {
let sec_0 = unwrap!(utils::generate_random_string(10));
let sec_1 = unwrap!(utils::generate_random_string(10));
let client_id = gen_client_id();
unwrap!(test_create_balance(
&client_id,
unwrap!(Coins::from_str("10"))
));
let dir = unwrap!(MDataInfo::random_private(MDataKind::Seq, DIR_TAG));
let dir_clone = dir.clone();
setup_client(
&(),
|el_h, core_tx, net_tx| {
AuthClient::registered(&sec_0, &sec_1, client_id, el_h, core_tx, net_tx)
},
move |client| {
assert!(client.set_access_container(dir));
client.update_account_packet()
},
);
setup_client(
&(),
|el_h, core_tx, net_tx| AuthClient::login(&sec_0, &sec_1, el_h, core_tx, net_tx),
move |client| {
let got_dir = client.access_container();
assert_eq!(got_dir, dir_clone);
finish()
},
);
}
#[test]
fn config_root_dir_creation() {
let sec_0 = unwrap!(utils::generate_random_string(10));
let sec_1 = unwrap!(utils::generate_random_string(10));
let client_id = gen_client_id();
unwrap!(test_create_balance(
&client_id,
unwrap!(Coins::from_str("10"))
));
let dir = unwrap!(MDataInfo::random_private(MDataKind::Seq, DIR_TAG));
let dir_clone = dir.clone();
setup_client(
&(),
|el_h, core_tx, net_tx| {
AuthClient::registered(&sec_0, &sec_1, client_id, el_h, core_tx, net_tx)
},
move |client| {
assert!(client.set_config_root_dir(dir));
client.update_account_packet()
},
);
setup_client(
&(),
|el_h, core_tx, net_tx| AuthClient::login(&sec_0, &sec_1, el_h, core_tx, net_tx),
move |client| {
let got_dir = client.config_root_dir();
assert_eq!(got_dir, dir_clone);
finish()
},
);
}
#[cfg(feature = "mock-network")]
#[ignore]
#[test]
fn restart_network() {
use crate::test_utils::random_client_with_net_obs;
use futures;
use safe_core::NetworkEvent;
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
let (hook, keep_alive) = futures::oneshot();
let _joiner = unwrap!(thread::Builder::new()
.name(String::from("Network Observer"))
.spawn(move || {
match unwrap!(rx.recv()) {
NetworkEvent::Disconnected => (),
x => panic!("Unexpected network event: {:?}", x),
}
match unwrap!(rx.recv()) {
NetworkEvent::Connected => (),
x => panic!("Unexpected network event: {:?}", x),
}
let _ = hook.send(());
}));
random_client_with_net_obs(
move |net_event| unwrap!(tx.send(net_event)),
move |client| {
client.simulate_network_disconnect();
unwrap!(client.restart_network());
keep_alive
},
);
}
#[cfg(feature = "mock-network")]
#[ignore]
#[test]
fn timeout() {
use crate::test_utils::random_client;
use safe_nd::{IDataAddress, PubImmutableData};
use std::time::Duration;
random_client(|client| {
let client2 = client.clone();
client.set_simulate_timeout(true);
client.set_timeout(Duration::from_millis(250));
client
.get_idata(IDataAddress::Pub(rand::random()))
.then(|result| match result {
Ok(_) => panic!("Unexpected success"),
Err(CoreError::RequestTimeout) => Ok::<_, CoreError>(()),
Err(err) => panic!("Unexpected {:?}", err),
})
.then(move |result| {
unwrap!(result);
let data = unwrap!(utils::generate_random_vector(4));
let data = PubImmutableData::new(data);
client2.put_idata(data)
})
.then(|result| match result {
Ok(_) => panic!("Unexpected success"),
Err(CoreError::RequestTimeout) => Ok::<_, CoreError>(()),
Err(err) => panic!("Unexpected {:?}", err),
})
})
}
#[test]
fn create_login_packet_for() {
let sec_0 = unwrap!(utils::generate_random_string(10));
let sec_1 = unwrap!(utils::generate_random_string(10));
let acc_locator: &[u8] = sec_0.as_bytes();
let acc_password: &[u8] = sec_1.as_bytes();
let (password, keyword, pin) = utils::derive_secrets(acc_locator, acc_password);
let acc_loc = unwrap!(Account::generate_network_id(&keyword, &pin));
let maid_keys = ClientKeys::new(&mut thread_rng());
let acc = unwrap!(Account::new(maid_keys.clone()));
let acc_ciphertext = unwrap!(acc.encrypt(&password, &pin));
let client_full_id = create_client_id(&acc_loc.0);
let sig = client_full_id.sign(&acc_ciphertext);
let client_pk = *client_full_id.public_id().public_key();
let new_login_packet = unwrap!(LoginPacket::new(acc_loc, client_pk, acc_ciphertext, sig));
let new_login_packet2 = new_login_packet.clone();
let five_coins = unwrap!(Coins::from_str("5"));
let client_id = gen_client_id();
let random_pk = *client_id.public_id().public_key();
let start_bal = unwrap!(Coins::from_str("10"));
random_client(move |client| {
let c1 = client.clone();
let c2 = client.clone();
let c3 = client.clone();
let c4 = client.clone();
client
.insert_login_packet_for(
None,
maid_keys.public_key(),
five_coins,
None,
new_login_packet.clone(),
)
.then(move |result| match result {
Ok(_transaction) => Ok::<_, CoreError>(()),
res => panic!("Unexpected {:?}", res),
})
.and_then(move |_| {
c1.insert_login_packet_for(
None,
maid_keys.public_key(),
unwrap!(Coins::from_str("3")),
None,
new_login_packet,
)
})
.then(move |result| match result {
Err(CoreError::DataError(SndError::BalanceExists)) => Ok::<_, CoreError>(()),
res => panic!("Unexpected {:?}", res),
})
.and_then(move |_| {
c3.insert_login_packet_for(
None,
random_pk,
unwrap!(Coins::from_str("3")),
None,
new_login_packet2,
)
})
.then(move |result| match result {
Err(CoreError::DataError(SndError::LoginPacketExists)) => {
c4.get_balance(Some(&client_id))
}
res => panic!("Unexpected {:?}", res),
})
.and_then(move |balance| {
assert_eq!(balance, unwrap!(Coins::from_str("3")));
c2.get_balance(None)
})
.and_then(move |balance| {
let expected = calculate_new_balance(
start_bal,
Some(3),
Some(unwrap!(Coins::from_str("8"))),
);
assert_eq!(balance, expected);
Ok(())
})
});
setup_client(
&(),
|el_h, core_tx, net_tx| AuthClient::login(&sec_0, &sec_1, el_h, core_tx, net_tx),
move |client| {
client.get_balance(None).and_then(move |balance| {
assert_eq!(balance, five_coins);
ok!(())
})
},
);
}
}