Merge #182
182: Segregate CLI specific modules r=D4nte a=D4nte To prepare for the introduction of nectar modules. Co-authored-by: Franck Royer <franck@coblox.tech>pull/183/head
commit
66d9c7c2cb
@ -1,121 +1,2 @@
|
|||||||
use crate::{bitcoin, monero};
|
pub mod command;
|
||||||
use libp2p::{core::Multiaddr, PeerId};
|
pub mod config;
|
||||||
use std::path::PathBuf;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
|
||||||
pub struct Options {
|
|
||||||
#[structopt(
|
|
||||||
long = "data-dir",
|
|
||||||
help = "Provide a custom path to the data directory.",
|
|
||||||
parse(from_os_str)
|
|
||||||
)]
|
|
||||||
pub data_dir: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[structopt(subcommand)]
|
|
||||||
pub cmd: Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
|
||||||
#[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")]
|
|
||||||
pub enum Command {
|
|
||||||
BuyXmr {
|
|
||||||
#[structopt(long = "connect-peer-id")]
|
|
||||||
alice_peer_id: PeerId,
|
|
||||||
|
|
||||||
#[structopt(long = "connect-addr")]
|
|
||||||
alice_addr: Multiaddr,
|
|
||||||
|
|
||||||
#[structopt(long = "send-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))]
|
|
||||||
send_bitcoin: bitcoin::Amount,
|
|
||||||
|
|
||||||
#[structopt(long = "receive-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))]
|
|
||||||
receive_monero: monero::Amount,
|
|
||||||
|
|
||||||
#[structopt(flatten)]
|
|
||||||
config: Config,
|
|
||||||
},
|
|
||||||
History,
|
|
||||||
Resume(Resume),
|
|
||||||
Cancel(Cancel),
|
|
||||||
Refund(Refund),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
|
||||||
pub enum Resume {
|
|
||||||
BuyXmr {
|
|
||||||
#[structopt(long = "swap-id")]
|
|
||||||
swap_id: Uuid,
|
|
||||||
|
|
||||||
#[structopt(long = "counterpart-peer-id")]
|
|
||||||
alice_peer_id: PeerId,
|
|
||||||
|
|
||||||
#[structopt(long = "counterpart-addr")]
|
|
||||||
alice_addr: Multiaddr,
|
|
||||||
|
|
||||||
#[structopt(flatten)]
|
|
||||||
config: Config,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
|
||||||
pub enum Cancel {
|
|
||||||
BuyXmr {
|
|
||||||
#[structopt(long = "swap-id")]
|
|
||||||
swap_id: Uuid,
|
|
||||||
|
|
||||||
// TODO: Remove Alice peer-id/address, it should be saved in the database when running swap
|
|
||||||
// and loaded from the database when running resume/cancel/refund
|
|
||||||
#[structopt(long = "counterpart-peer-id")]
|
|
||||||
alice_peer_id: PeerId,
|
|
||||||
#[structopt(long = "counterpart-addr")]
|
|
||||||
alice_addr: Multiaddr,
|
|
||||||
|
|
||||||
#[structopt(flatten)]
|
|
||||||
config: Config,
|
|
||||||
|
|
||||||
#[structopt(short, long)]
|
|
||||||
force: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
|
||||||
pub enum Refund {
|
|
||||||
BuyXmr {
|
|
||||||
#[structopt(long = "swap-id")]
|
|
||||||
swap_id: Uuid,
|
|
||||||
|
|
||||||
// TODO: Remove Alice peer-id/address, it should be saved in the database when running swap
|
|
||||||
// and loaded from the database when running resume/cancel/refund
|
|
||||||
#[structopt(long = "counterpart-peer-id")]
|
|
||||||
alice_peer_id: PeerId,
|
|
||||||
#[structopt(long = "counterpart-addr")]
|
|
||||||
alice_addr: Multiaddr,
|
|
||||||
|
|
||||||
#[structopt(flatten)]
|
|
||||||
config: Config,
|
|
||||||
|
|
||||||
#[structopt(short, long)]
|
|
||||||
force: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
#[structopt(
|
|
||||||
long = "config",
|
|
||||||
help = "Provide a custom path to the configuration file. The configuration file must be a toml file.",
|
|
||||||
parse(from_os_str)
|
|
||||||
)]
|
|
||||||
pub path: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_btc(str: &str) -> anyhow::Result<bitcoin::Amount> {
|
|
||||||
let amount = bitcoin::Amount::from_str_in(str, ::bitcoin::Denomination::Bitcoin)?;
|
|
||||||
Ok(amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_xmr(str: &str) -> anyhow::Result<monero::Amount> {
|
|
||||||
let amount = monero::Amount::parse_monero(str)?;
|
|
||||||
Ok(amount)
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,121 @@
|
|||||||
|
use crate::{bitcoin, monero};
|
||||||
|
use libp2p::{core::Multiaddr, PeerId};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
|
pub struct Arguments {
|
||||||
|
#[structopt(
|
||||||
|
long = "data-dir",
|
||||||
|
help = "Provide a custom path to the data directory.",
|
||||||
|
parse(from_os_str)
|
||||||
|
)]
|
||||||
|
pub data_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[structopt(subcommand)]
|
||||||
|
pub cmd: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
|
#[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")]
|
||||||
|
pub enum Command {
|
||||||
|
BuyXmr {
|
||||||
|
#[structopt(long = "connect-peer-id")]
|
||||||
|
alice_peer_id: PeerId,
|
||||||
|
|
||||||
|
#[structopt(long = "connect-addr")]
|
||||||
|
alice_addr: Multiaddr,
|
||||||
|
|
||||||
|
#[structopt(long = "send-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))]
|
||||||
|
send_bitcoin: bitcoin::Amount,
|
||||||
|
|
||||||
|
#[structopt(long = "receive-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))]
|
||||||
|
receive_monero: monero::Amount,
|
||||||
|
|
||||||
|
#[structopt(flatten)]
|
||||||
|
config: Config,
|
||||||
|
},
|
||||||
|
History,
|
||||||
|
Resume(Resume),
|
||||||
|
Cancel(Cancel),
|
||||||
|
Refund(Refund),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
|
pub enum Resume {
|
||||||
|
BuyXmr {
|
||||||
|
#[structopt(long = "swap-id")]
|
||||||
|
swap_id: Uuid,
|
||||||
|
|
||||||
|
#[structopt(long = "counterpart-peer-id")]
|
||||||
|
alice_peer_id: PeerId,
|
||||||
|
|
||||||
|
#[structopt(long = "counterpart-addr")]
|
||||||
|
alice_addr: Multiaddr,
|
||||||
|
|
||||||
|
#[structopt(flatten)]
|
||||||
|
config: Config,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
|
pub enum Cancel {
|
||||||
|
BuyXmr {
|
||||||
|
#[structopt(long = "swap-id")]
|
||||||
|
swap_id: Uuid,
|
||||||
|
|
||||||
|
// TODO: Remove Alice peer-id/address, it should be saved in the database when running swap
|
||||||
|
// and loaded from the database when running resume/cancel/refund
|
||||||
|
#[structopt(long = "counterpart-peer-id")]
|
||||||
|
alice_peer_id: PeerId,
|
||||||
|
#[structopt(long = "counterpart-addr")]
|
||||||
|
alice_addr: Multiaddr,
|
||||||
|
|
||||||
|
#[structopt(flatten)]
|
||||||
|
config: Config,
|
||||||
|
|
||||||
|
#[structopt(short, long)]
|
||||||
|
force: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
|
pub enum Refund {
|
||||||
|
BuyXmr {
|
||||||
|
#[structopt(long = "swap-id")]
|
||||||
|
swap_id: Uuid,
|
||||||
|
|
||||||
|
// TODO: Remove Alice peer-id/address, it should be saved in the database when running swap
|
||||||
|
// and loaded from the database when running resume/cancel/refund
|
||||||
|
#[structopt(long = "counterpart-peer-id")]
|
||||||
|
alice_peer_id: PeerId,
|
||||||
|
#[structopt(long = "counterpart-addr")]
|
||||||
|
alice_addr: Multiaddr,
|
||||||
|
|
||||||
|
#[structopt(flatten)]
|
||||||
|
config: Config,
|
||||||
|
|
||||||
|
#[structopt(short, long)]
|
||||||
|
force: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
#[structopt(
|
||||||
|
long = "config",
|
||||||
|
help = "Provide a custom path to the configuration file. The configuration file must be a toml file.",
|
||||||
|
parse(from_os_str)
|
||||||
|
)]
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_btc(str: &str) -> anyhow::Result<bitcoin::Amount> {
|
||||||
|
let amount = bitcoin::Amount::from_str_in(str, ::bitcoin::Denomination::Bitcoin)?;
|
||||||
|
Ok(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_xmr(str: &str) -> anyhow::Result<monero::Amount> {
|
||||||
|
let amount = monero::Amount::parse_monero(str)?;
|
||||||
|
Ok(amount)
|
||||||
|
}
|
@ -1,196 +0,0 @@
|
|||||||
use crate::{fs::ensure_directory_exists, seed};
|
|
||||||
use pem::{encode, Pem};
|
|
||||||
use seed::SEED_LENGTH;
|
|
||||||
use std::{
|
|
||||||
ffi::OsStr,
|
|
||||||
fmt,
|
|
||||||
fs::{self, File},
|
|
||||||
io::{self, Write},
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
pub struct Seed(seed::Seed);
|
|
||||||
|
|
||||||
impl Seed {
|
|
||||||
pub fn random() -> Result<Self, Error> {
|
|
||||||
Ok(Seed(seed::Seed::random()?))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_file_or_generate(data_dir: &Path) -> Result<Self, Error> {
|
|
||||||
let file_path_buf = data_dir.join("seed.pem");
|
|
||||||
let file_path = Path::new(&file_path_buf);
|
|
||||||
|
|
||||||
if file_path.exists() {
|
|
||||||
return Self::from_file(&file_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!("No seed file found, creating at: {}", file_path.display());
|
|
||||||
|
|
||||||
let random_seed = Seed::random()?;
|
|
||||||
random_seed.write_to(file_path.to_path_buf())?;
|
|
||||||
|
|
||||||
Ok(random_seed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_file<D>(seed_file: D) -> Result<Self, Error>
|
|
||||||
where
|
|
||||||
D: AsRef<OsStr>,
|
|
||||||
{
|
|
||||||
let file = Path::new(&seed_file);
|
|
||||||
let contents = fs::read_to_string(file)?;
|
|
||||||
let pem = pem::parse(contents)?;
|
|
||||||
|
|
||||||
tracing::info!("Read in seed from file: {}", file.display());
|
|
||||||
|
|
||||||
Self::from_pem(pem)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_pem(pem: pem::Pem) -> Result<Self, Error> {
|
|
||||||
if pem.contents.len() != SEED_LENGTH {
|
|
||||||
Err(Error::IncorrectLength(pem.contents.len()))
|
|
||||||
} else {
|
|
||||||
let mut array = [0; SEED_LENGTH];
|
|
||||||
for (i, b) in pem.contents.iter().enumerate() {
|
|
||||||
array[i] = *b;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self::from(array))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_to(&self, seed_file: PathBuf) -> Result<(), Error> {
|
|
||||||
ensure_directory_exists(&seed_file)?;
|
|
||||||
|
|
||||||
let data = (self.0).bytes();
|
|
||||||
let pem = Pem {
|
|
||||||
tag: String::from("SEED"),
|
|
||||||
contents: data.to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let pem_string = encode(&pem);
|
|
||||||
|
|
||||||
let mut file = File::create(seed_file)?;
|
|
||||||
file.write_all(pem_string.as_bytes())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Seed {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "Seed([*****])")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Seed {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{:?}", self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<[u8; SEED_LENGTH]> for Seed {
|
|
||||||
fn from(bytes: [u8; 32]) -> Self {
|
|
||||||
Seed(seed::Seed::from(bytes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Seed> for seed::Seed {
|
|
||||||
fn from(seed: Seed) -> Self {
|
|
||||||
seed.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Seed generation: ")]
|
|
||||||
SeedGeneration(#[from] crate::seed::Error),
|
|
||||||
#[error("io: ")]
|
|
||||||
Io(#[from] io::Error),
|
|
||||||
#[error("PEM parse: ")]
|
|
||||||
PemParse(#[from] pem::PemError),
|
|
||||||
#[error("expected 32 bytes of base64 encode, got {0} bytes")]
|
|
||||||
IncorrectLength(usize),
|
|
||||||
#[error("RNG: ")]
|
|
||||||
Rand(#[from] rand::Error),
|
|
||||||
#[error("no default path")]
|
|
||||||
NoDefaultPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::env::temp_dir;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn seed_byte_string_must_be_32_bytes_long() {
|
|
||||||
let _seed = Seed::from(*b"this string is exactly 32 bytes!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn seed_from_pem_works() {
|
|
||||||
let payload: &str = "syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM=";
|
|
||||||
|
|
||||||
// 32 bytes base64 encoded.
|
|
||||||
let pem_string: &str = "-----BEGIN SEED-----
|
|
||||||
syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM=
|
|
||||||
-----END SEED-----
|
|
||||||
";
|
|
||||||
|
|
||||||
let want = base64::decode(payload).unwrap();
|
|
||||||
let pem = pem::parse(pem_string).unwrap();
|
|
||||||
let got = Seed::from_pem(pem).unwrap();
|
|
||||||
|
|
||||||
assert_eq!((got.0).bytes(), *want);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn seed_from_pem_fails_for_short_seed() {
|
|
||||||
let short = "-----BEGIN SEED-----
|
|
||||||
VnZUNFZ4dlY=
|
|
||||||
-----END SEED-----
|
|
||||||
";
|
|
||||||
let pem = pem::parse(short).unwrap();
|
|
||||||
match Seed::from_pem(pem) {
|
|
||||||
Ok(_) => panic!("should fail for short payload"),
|
|
||||||
Err(e) => {
|
|
||||||
match e {
|
|
||||||
Error::IncorrectLength(_) => {} // pass
|
|
||||||
_ => panic!("should fail with IncorrectLength error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn seed_from_pem_fails_for_long_seed() {
|
|
||||||
let long = "-----BEGIN SEED-----
|
|
||||||
mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE=
|
|
||||||
mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE=
|
|
||||||
-----END SEED-----
|
|
||||||
";
|
|
||||||
let pem = pem::parse(long).unwrap();
|
|
||||||
match Seed::from_pem(pem) {
|
|
||||||
Ok(_) => panic!("should fail for long payload"),
|
|
||||||
Err(e) => {
|
|
||||||
match e {
|
|
||||||
Error::IncorrectLength(_) => {} // pass
|
|
||||||
_ => panic!("should fail with IncorrectLength error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn round_trip_through_file_write_read() {
|
|
||||||
let tmpfile = temp_dir().join("seed.pem");
|
|
||||||
|
|
||||||
let seed = Seed::random().unwrap();
|
|
||||||
seed.write_to(tmpfile.clone())
|
|
||||||
.expect("Write seed to temp file");
|
|
||||||
|
|
||||||
let rinsed = Seed::from_file(tmpfile).expect("Read from temp file");
|
|
||||||
assert_eq!(seed.0, rinsed.0);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue