2021-03-24 22:49:09 +01:00
|
|
|
extern crate rpassword;
|
2021-08-01 14:43:18 +02:00
|
|
|
use clap::{crate_version, App, Arg};
|
2021-03-27 15:35:52 +01:00
|
|
|
use log::*;
|
2021-04-04 16:40:16 +02:00
|
|
|
use regex::Regex;
|
2021-08-01 14:43:18 +02:00
|
|
|
use std::collections::HashSet;
|
|
|
|
use std::{
|
2021-08-08 18:54:46 +02:00
|
|
|
io::{stdin, stdout, Write},
|
|
|
|
path::Path,
|
|
|
|
sync::{Arc, Mutex},
|
2021-08-01 14:43:18 +02:00
|
|
|
};
|
2021-08-08 18:34:06 +02:00
|
|
|
use steamguard::{
|
2021-08-08 19:37:18 +02:00
|
|
|
steamapi, AccountLinker, Confirmation, ConfirmationType, LoginError, SteamGuardAccount,
|
|
|
|
UserLogin,
|
2021-08-08 18:34:06 +02:00
|
|
|
};
|
2021-08-01 14:43:18 +02:00
|
|
|
use termion::{
|
2021-08-08 18:54:46 +02:00
|
|
|
event::{Event, Key},
|
|
|
|
input::TermRead,
|
|
|
|
raw::IntoRawMode,
|
|
|
|
screen::AlternateScreen,
|
2021-08-01 14:43:18 +02:00
|
|
|
};
|
2021-03-24 22:49:09 +01:00
|
|
|
|
2021-04-04 16:40:16 +02:00
|
|
|
#[macro_use]
|
|
|
|
extern crate lazy_static;
|
2021-08-01 17:20:57 +02:00
|
|
|
#[macro_use]
|
|
|
|
extern crate anyhow;
|
2021-08-01 14:43:18 +02:00
|
|
|
mod accountmanager;
|
2021-03-22 02:21:29 +01:00
|
|
|
|
2021-04-04 16:40:16 +02:00
|
|
|
lazy_static! {
|
2021-08-08 18:54:46 +02:00
|
|
|
static ref CAPTCHA_VALID_CHARS: Regex =
|
|
|
|
Regex::new("^([A-H]|[J-N]|[P-R]|[T-Z]|[2-4]|[7-9]|[@%&])+$").unwrap();
|
2021-04-04 16:40:16 +02:00
|
|
|
}
|
|
|
|
|
2021-03-22 02:21:29 +01:00
|
|
|
fn main() {
|
2021-08-08 18:54:46 +02:00
|
|
|
let matches = App::new("steamguard-cli")
|
2021-03-27 14:31:38 +01:00
|
|
|
.version(crate_version!())
|
|
|
|
.bin_name("steamguard")
|
|
|
|
.author("dyc3 (Carson McManus)")
|
|
|
|
.about("Generate Steam 2FA codes and confirm Steam trades from the command line.")
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("username")
|
|
|
|
.long("username")
|
|
|
|
.short("u")
|
|
|
|
.help("Select the account you want by steam username. By default, the first account in the manifest is selected.")
|
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("all")
|
|
|
|
.long("all")
|
|
|
|
.short("a")
|
2021-03-27 17:14:34 +01:00
|
|
|
.takes_value(false)
|
2021-03-27 14:31:38 +01:00
|
|
|
.help("Select all accounts in the manifest.")
|
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("mafiles-path")
|
|
|
|
.long("mafiles-path")
|
|
|
|
.short("m")
|
|
|
|
.default_value("~/maFiles")
|
|
|
|
.help("Specify which folder your maFiles are in.")
|
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("passkey")
|
|
|
|
.long("passkey")
|
|
|
|
.short("p")
|
|
|
|
.help("Specify your encryption passkey.")
|
|
|
|
)
|
2021-03-27 15:35:52 +01:00
|
|
|
.arg(
|
|
|
|
Arg::with_name("verbosity")
|
|
|
|
.short("v")
|
|
|
|
.help("Log what is going on verbosely.")
|
|
|
|
.takes_value(false)
|
|
|
|
.multiple(true)
|
|
|
|
)
|
2021-03-27 14:31:38 +01:00
|
|
|
.subcommand(
|
|
|
|
App::new("trade")
|
|
|
|
.about("Interactive interface for trade confirmations")
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("accept-all")
|
|
|
|
.short("a")
|
|
|
|
.long("accept-all")
|
2021-03-27 17:14:34 +01:00
|
|
|
.takes_value(false)
|
2021-03-27 14:31:38 +01:00
|
|
|
.help("Accept all open trade confirmations. Does not open interactive interface.")
|
|
|
|
)
|
|
|
|
)
|
2021-07-27 22:24:56 +02:00
|
|
|
.subcommand(
|
|
|
|
App::new("setup")
|
|
|
|
.about("Set up a new account with steamguard-cli")
|
|
|
|
)
|
2021-07-30 01:42:45 +02:00
|
|
|
.subcommand(
|
|
|
|
App::new("debug")
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("demo-conf-menu")
|
|
|
|
.help("Show an example confirmation menu using dummy data.")
|
|
|
|
.takes_value(false)
|
|
|
|
)
|
|
|
|
)
|
2021-03-27 14:31:38 +01:00
|
|
|
.get_matches();
|
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
let verbosity = matches.occurrences_of("verbosity") as usize + 2;
|
|
|
|
stderrlog::new()
|
|
|
|
.verbosity(verbosity)
|
|
|
|
.module(module_path!())
|
|
|
|
.init()
|
|
|
|
.unwrap();
|
2021-03-22 02:21:29 +01:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
if let Some(demo_matches) = matches.subcommand_matches("debug") {
|
|
|
|
if demo_matches.is_present("demo-conf-menu") {
|
|
|
|
demo_confirmation_menu();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2021-07-30 01:42:45 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
let path = Path::new(matches.value_of("mafiles-path").unwrap()).join("manifest.json");
|
|
|
|
let mut manifest: accountmanager::Manifest;
|
|
|
|
match accountmanager::Manifest::load(path.as_path()) {
|
|
|
|
Ok(m) => {
|
|
|
|
manifest = m;
|
|
|
|
}
|
|
|
|
Err(e) => {
|
|
|
|
error!("Could not load manifest: {}", e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2021-03-27 15:35:52 +01:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
manifest.load_accounts();
|
2021-07-27 22:24:56 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
if matches.is_present("setup") {
|
|
|
|
info!("setup");
|
2021-08-05 03:26:14 +02:00
|
|
|
let mut linker = AccountLinker::new();
|
2021-08-08 18:54:46 +02:00
|
|
|
// do_login(&mut linker.account);
|
|
|
|
// linker.link(linker.account.session.expect("no login session"));
|
|
|
|
return;
|
|
|
|
}
|
2021-07-27 22:24:56 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
let mut selected_accounts: Vec<Arc<Mutex<SteamGuardAccount>>> = vec![];
|
|
|
|
if matches.is_present("all") {
|
|
|
|
// manifest.accounts.iter().map(|a| selected_accounts.push(a.b));
|
|
|
|
for account in &manifest.accounts {
|
|
|
|
selected_accounts.push(account.clone());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for account in &manifest.accounts {
|
|
|
|
if !matches.is_present("username") {
|
|
|
|
selected_accounts.push(account.clone());
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if matches.value_of("username").unwrap() == account.lock().unwrap().account_name {
|
|
|
|
selected_accounts.push(account.clone());
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-27 17:14:34 +01:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
debug!(
|
|
|
|
"selected accounts: {:?}",
|
|
|
|
selected_accounts
|
|
|
|
.iter()
|
|
|
|
.map(|a| a.lock().unwrap().account_name.clone())
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
);
|
2021-03-27 17:14:34 +01:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
if let Some(trade_matches) = matches.subcommand_matches("trade") {
|
|
|
|
info!("trade");
|
|
|
|
for a in selected_accounts.iter_mut() {
|
|
|
|
let mut account = a.lock().unwrap();
|
2021-04-04 20:07:06 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
info!("Checking for trade confirmations");
|
|
|
|
let confirmations: Vec<Confirmation>;
|
|
|
|
loop {
|
|
|
|
match account.get_trade_confirmations() {
|
|
|
|
Ok(confs) => {
|
|
|
|
confirmations = confs;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
Err(_) => {
|
|
|
|
info!("failed to get trade confirmations, asking user to log in");
|
|
|
|
do_login(&mut account);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-07-29 15:08:06 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
if trade_matches.is_present("accept-all") {
|
|
|
|
info!("accepting all confirmations");
|
|
|
|
for conf in &confirmations {
|
|
|
|
let result = account.accept_confirmation(conf);
|
|
|
|
debug!("accept confirmation result: {:?}", result);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if termion::is_tty(&stdout()) {
|
|
|
|
let (accept, deny) = prompt_confirmation_menu(confirmations);
|
|
|
|
for conf in &accept {
|
|
|
|
let result = account.accept_confirmation(conf);
|
|
|
|
debug!("accept confirmation result: {:?}", result);
|
|
|
|
}
|
|
|
|
for conf in &deny {
|
|
|
|
let result = account.deny_confirmation(conf);
|
|
|
|
debug!("deny confirmation result: {:?}", result);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
warn!("not a tty, not showing menu");
|
|
|
|
for conf in &confirmations {
|
|
|
|
println!("{}", conf.description());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-08-01 17:20:57 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
manifest.save();
|
|
|
|
} else {
|
|
|
|
let server_time = steamapi::get_server_time();
|
|
|
|
for account in selected_accounts {
|
|
|
|
trace!("{:?}", account);
|
|
|
|
let code = account.lock().unwrap().generate_code(server_time);
|
|
|
|
println!("{}", code);
|
|
|
|
}
|
|
|
|
}
|
2021-03-22 02:21:29 +01:00
|
|
|
}
|
2021-04-04 16:40:16 +02:00
|
|
|
|
|
|
|
fn validate_captcha_text(text: &String) -> bool {
|
2021-08-08 18:54:46 +02:00
|
|
|
return CAPTCHA_VALID_CHARS.is_match(text);
|
2021-04-04 16:40:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_validate_captcha_text() {
|
2021-08-08 18:54:46 +02:00
|
|
|
assert!(validate_captcha_text(&String::from("2WWUA@")));
|
|
|
|
assert!(validate_captcha_text(&String::from("3G8HT2")));
|
|
|
|
assert!(validate_captcha_text(&String::from("3J%@X3")));
|
|
|
|
assert!(validate_captcha_text(&String::from("2GCZ4A")));
|
|
|
|
assert!(validate_captcha_text(&String::from("3G8HT2")));
|
|
|
|
assert!(!validate_captcha_text(&String::from("asd823")));
|
|
|
|
assert!(!validate_captcha_text(&String::from("!PQ4RD")));
|
|
|
|
assert!(!validate_captcha_text(&String::from("1GQ4XZ")));
|
|
|
|
assert!(!validate_captcha_text(&String::from("8GO4XZ")));
|
|
|
|
assert!(!validate_captcha_text(&String::from("IPQ4RD")));
|
|
|
|
assert!(!validate_captcha_text(&String::from("0PT4RD")));
|
|
|
|
assert!(!validate_captcha_text(&String::from("APTSRD")));
|
|
|
|
assert!(!validate_captcha_text(&String::from("AP5TRD")));
|
|
|
|
assert!(!validate_captcha_text(&String::from("AP6TRD")));
|
2021-04-04 16:40:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Prompt the user for text input.
|
|
|
|
fn prompt() -> String {
|
2021-08-08 18:54:46 +02:00
|
|
|
let mut text = String::new();
|
|
|
|
let _ = std::io::stdout().flush();
|
|
|
|
stdin()
|
|
|
|
.read_line(&mut text)
|
|
|
|
.expect("Did not enter a correct string");
|
|
|
|
return String::from(text.strip_suffix('\n').unwrap());
|
2021-04-04 16:40:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fn prompt_captcha_text(captcha_gid: &String) -> String {
|
2021-08-08 18:54:46 +02:00
|
|
|
println!("Captcha required. Open this link in your web browser: https://steamcommunity.com/public/captcha.php?gid={}", captcha_gid);
|
|
|
|
let mut captcha_text;
|
|
|
|
loop {
|
|
|
|
print!("Enter captcha text: ");
|
|
|
|
captcha_text = prompt();
|
|
|
|
if captcha_text.len() > 0 && validate_captcha_text(&captcha_text) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
warn!("Invalid chars for captcha text found in user's input. Prompting again...");
|
|
|
|
}
|
|
|
|
return captcha_text;
|
2021-04-04 16:40:16 +02:00
|
|
|
}
|
2021-07-27 22:24:56 +02:00
|
|
|
|
2021-07-30 01:42:45 +02:00
|
|
|
/// Returns a tuple of (accepted, denied). Ignored confirmations are not included.
|
2021-08-01 14:43:18 +02:00
|
|
|
fn prompt_confirmation_menu(
|
2021-08-08 18:54:46 +02:00
|
|
|
confirmations: Vec<Confirmation>,
|
2021-08-01 14:43:18 +02:00
|
|
|
) -> (Vec<Confirmation>, Vec<Confirmation>) {
|
2021-08-08 18:54:46 +02:00
|
|
|
println!("press a key other than enter to show the menu.");
|
|
|
|
let mut to_accept_idx: HashSet<usize> = HashSet::new();
|
|
|
|
let mut to_deny_idx: HashSet<usize> = HashSet::new();
|
2021-07-30 01:42:45 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
let mut screen = AlternateScreen::from(stdout().into_raw_mode().unwrap());
|
|
|
|
let stdin = stdin();
|
2021-07-30 01:42:45 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
let mut selected_idx = 0;
|
2021-07-30 01:42:45 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
for c in stdin.events() {
|
|
|
|
match c.expect("could not get events") {
|
|
|
|
Event::Key(Key::Char('a')) => {
|
|
|
|
to_accept_idx.insert(selected_idx);
|
|
|
|
to_deny_idx.remove(&selected_idx);
|
|
|
|
}
|
|
|
|
Event::Key(Key::Char('d')) => {
|
|
|
|
to_accept_idx.remove(&selected_idx);
|
|
|
|
to_deny_idx.insert(selected_idx);
|
|
|
|
}
|
|
|
|
Event::Key(Key::Char('i')) => {
|
|
|
|
to_accept_idx.remove(&selected_idx);
|
|
|
|
to_deny_idx.remove(&selected_idx);
|
|
|
|
}
|
|
|
|
Event::Key(Key::Char('A')) => {
|
|
|
|
(0..confirmations.len()).for_each(|i| {
|
|
|
|
to_accept_idx.insert(i);
|
|
|
|
to_deny_idx.remove(&i);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
Event::Key(Key::Char('D')) => {
|
|
|
|
(0..confirmations.len()).for_each(|i| {
|
|
|
|
to_accept_idx.remove(&i);
|
|
|
|
to_deny_idx.insert(i);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
Event::Key(Key::Char('I')) => {
|
|
|
|
(0..confirmations.len()).for_each(|i| {
|
|
|
|
to_accept_idx.remove(&i);
|
|
|
|
to_deny_idx.remove(&i);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
Event::Key(Key::Up) if selected_idx > 0 => {
|
|
|
|
selected_idx -= 1;
|
|
|
|
}
|
|
|
|
Event::Key(Key::Down) if selected_idx < confirmations.len() - 1 => {
|
|
|
|
selected_idx += 1;
|
|
|
|
}
|
|
|
|
Event::Key(Key::Char('\n')) => {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
Event::Key(Key::Esc) | Event::Key(Key::Ctrl('c')) => {
|
|
|
|
return (vec![], vec![]);
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
2021-07-30 01:42:45 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
write!(
|
|
|
|
screen,
|
|
|
|
"{}{}{}arrow keys to select, [a]ccept, [d]eny, [i]gnore, [enter] confirm choices\n\n",
|
|
|
|
termion::clear::All,
|
|
|
|
termion::cursor::Goto(1, 1),
|
|
|
|
termion::color::Fg(termion::color::White)
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
for i in 0..confirmations.len() {
|
|
|
|
if selected_idx == i {
|
|
|
|
write!(
|
|
|
|
screen,
|
|
|
|
"\r{} >",
|
|
|
|
termion::color::Fg(termion::color::LightYellow)
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
} else {
|
|
|
|
write!(screen, "\r{} ", termion::color::Fg(termion::color::White)).unwrap();
|
|
|
|
}
|
2021-07-30 01:42:45 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
if to_accept_idx.contains(&i) {
|
|
|
|
write!(
|
|
|
|
screen,
|
|
|
|
"{}[a]",
|
|
|
|
termion::color::Fg(termion::color::LightGreen)
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
} else if to_deny_idx.contains(&i) {
|
|
|
|
write!(
|
|
|
|
screen,
|
|
|
|
"{}[d]",
|
|
|
|
termion::color::Fg(termion::color::LightRed)
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
} else {
|
|
|
|
write!(screen, "[ ]").unwrap();
|
|
|
|
}
|
2021-07-30 01:42:45 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
if selected_idx == i {
|
|
|
|
write!(
|
|
|
|
screen,
|
|
|
|
"{}",
|
|
|
|
termion::color::Fg(termion::color::LightYellow)
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
}
|
2021-07-30 01:42:45 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
write!(screen, " {}\n", confirmations[i].description()).unwrap();
|
|
|
|
}
|
|
|
|
}
|
2021-07-30 01:42:45 +02:00
|
|
|
|
2021-08-08 18:54:46 +02:00
|
|
|
return (
|
|
|
|
to_accept_idx.iter().map(|i| confirmations[*i]).collect(),
|
|
|
|
to_deny_idx.iter().map(|i| confirmations[*i]).collect(),
|
|
|
|
);
|
2021-07-30 01:42:45 +02:00
|
|
|
}
|
|
|
|
|
2021-07-27 22:24:56 +02:00
|
|
|
fn do_login(account: &mut SteamGuardAccount) {
|
2021-08-08 18:54:46 +02:00
|
|
|
if account.account_name.len() > 0 {
|
|
|
|
println!("Username: {}", account.account_name);
|
|
|
|
} else {
|
|
|
|
print!("Username: ");
|
|
|
|
account.account_name = prompt();
|
|
|
|
}
|
|
|
|
let _ = std::io::stdout().flush();
|
|
|
|
let password = rpassword::prompt_password_stdout("Password: ").unwrap();
|
|
|
|
if password.len() > 0 {
|
|
|
|
debug!("password is present");
|
|
|
|
} else {
|
|
|
|
debug!("password is empty");
|
|
|
|
}
|
|
|
|
// TODO: reprompt if password is empty
|
|
|
|
let mut login = UserLogin::new(account.account_name.clone(), password);
|
|
|
|
let mut loops = 0;
|
|
|
|
loop {
|
|
|
|
match login.login() {
|
|
|
|
Ok(s) => {
|
|
|
|
account.session = Option::Some(s);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
Err(LoginError::Need2FA) => {
|
|
|
|
debug!("generating 2fa code and retrying");
|
|
|
|
let server_time = steamapi::get_server_time();
|
|
|
|
login.twofactor_code = account.generate_code(server_time);
|
|
|
|
}
|
|
|
|
Err(LoginError::NeedCaptcha { captcha_gid }) => {
|
|
|
|
debug!("need captcha to log in");
|
|
|
|
login.captcha_text = prompt_captcha_text(&captcha_gid);
|
|
|
|
}
|
|
|
|
Err(LoginError::NeedEmail) => {
|
|
|
|
println!("You should have received an email with a code.");
|
|
|
|
print!("Enter code");
|
|
|
|
login.email_code = prompt();
|
|
|
|
}
|
|
|
|
r => {
|
|
|
|
error!("Fatal login result: {:?}", r);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
loops += 1;
|
|
|
|
if loops > 2 {
|
|
|
|
error!("Too many loops. Aborting login process, to avoid getting rate limited.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2021-07-27 22:24:56 +02:00
|
|
|
}
|
2021-07-30 01:42:45 +02:00
|
|
|
|
|
|
|
fn demo_confirmation_menu() {
|
2021-08-08 18:54:46 +02:00
|
|
|
info!("showing demo menu");
|
|
|
|
let (accept, deny) = prompt_confirmation_menu(vec![
|
|
|
|
Confirmation {
|
|
|
|
id: 1234,
|
|
|
|
key: 12345,
|
|
|
|
conf_type: ConfirmationType::Trade,
|
|
|
|
creator: 09870987,
|
|
|
|
},
|
|
|
|
Confirmation {
|
|
|
|
id: 1234,
|
|
|
|
key: 12345,
|
|
|
|
conf_type: ConfirmationType::MarketSell,
|
|
|
|
creator: 09870987,
|
|
|
|
},
|
|
|
|
Confirmation {
|
|
|
|
id: 1234,
|
|
|
|
key: 12345,
|
|
|
|
conf_type: ConfirmationType::AccountRecovery,
|
|
|
|
creator: 09870987,
|
|
|
|
},
|
|
|
|
Confirmation {
|
|
|
|
id: 1234,
|
|
|
|
key: 12345,
|
|
|
|
conf_type: ConfirmationType::Trade,
|
|
|
|
creator: 09870987,
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
println!("accept: {}, deny: {}", accept.len(), deny.len());
|
2021-07-30 01:42:45 +02:00
|
|
|
}
|