mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
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>
This commit is contained in:
commit
66d9c7c2cb
@ -19,10 +19,11 @@ use std::{path::PathBuf, sync::Arc};
|
||||
use structopt::StructOpt;
|
||||
use swap::{
|
||||
bitcoin,
|
||||
cli::{Cancel, Command, Options, Refund, Resume},
|
||||
config,
|
||||
config::{
|
||||
initial_setup, query_user_for_initial_testnet_config, read_config, ConfigNotInitialized,
|
||||
cli::{
|
||||
command::{Arguments, Cancel, Command, Refund, Resume},
|
||||
config::{
|
||||
initial_setup, query_user_for_initial_testnet_config, read_config, ConfigNotInitialized,
|
||||
},
|
||||
},
|
||||
database::Database,
|
||||
execution_params,
|
||||
@ -35,6 +36,7 @@ use swap::{
|
||||
bob::{cancel::CancelError, Builder},
|
||||
SwapAmounts,
|
||||
},
|
||||
seed::Seed,
|
||||
trace::init_tracing,
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
@ -49,7 +51,7 @@ const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-mon
|
||||
async fn main() -> Result<()> {
|
||||
init_tracing(LevelFilter::Debug).expect("initialize tracing");
|
||||
|
||||
let opt = Options::from_args();
|
||||
let opt = Arguments::from_args();
|
||||
|
||||
let data_dir = if let Some(data_dir) = opt.data_dir {
|
||||
data_dir
|
||||
@ -63,9 +65,7 @@ async fn main() -> Result<()> {
|
||||
);
|
||||
|
||||
let db_path = data_dir.join("database");
|
||||
let seed = config::Seed::from_file_or_generate(&data_dir)
|
||||
.expect("Could not retrieve/initialize seed")
|
||||
.into();
|
||||
let seed = Seed::from_file_or_generate(&data_dir).expect("Could not retrieve/initialize seed");
|
||||
|
||||
// hardcode to testnet/stagenet
|
||||
let bitcoin_network = bitcoin::Network::Testnet;
|
||||
|
123
swap/src/cli.rs
123
swap/src/cli.rs
@ -1,121 +1,2 @@
|
||||
use crate::{bitcoin, monero};
|
||||
use libp2p::{core::Multiaddr, PeerId};
|
||||
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)
|
||||
}
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
|
121
swap/src/cli/command.rs
Normal file
121
swap/src/cli/command.rs
Normal file
@ -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,6 +1,6 @@
|
||||
use crate::fs::ensure_directory_exists;
|
||||
use anyhow::{Context, Result};
|
||||
use config::{Config, ConfigError};
|
||||
use config::ConfigError;
|
||||
use dialoguer::{theme::ColorfulTheme, Input};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@ -11,27 +11,23 @@ use std::{
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
|
||||
pub mod seed;
|
||||
|
||||
pub use seed::Seed;
|
||||
|
||||
const DEFAULT_BITCOIND_TESTNET_URL: &str = "http://127.0.0.1:18332";
|
||||
const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc";
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
pub struct File {
|
||||
pub struct Config {
|
||||
pub bitcoin: Bitcoin,
|
||||
pub monero: Monero,
|
||||
}
|
||||
|
||||
impl File {
|
||||
impl Config {
|
||||
pub fn read<D>(config_file: D) -> Result<Self, ConfigError>
|
||||
where
|
||||
D: AsRef<OsStr>,
|
||||
{
|
||||
let config_file = Path::new(&config_file);
|
||||
|
||||
let mut config = Config::new();
|
||||
let mut config = config::Config::new();
|
||||
config.merge(config::File::from(config_file))?;
|
||||
config.try_into()
|
||||
}
|
||||
@ -54,7 +50,7 @@ pub struct Monero {
|
||||
#[error("config not initialized")]
|
||||
pub struct ConfigNotInitialized {}
|
||||
|
||||
pub fn read_config(config_path: PathBuf) -> Result<Result<File, ConfigNotInitialized>> {
|
||||
pub fn read_config(config_path: PathBuf) -> Result<Result<Config, ConfigNotInitialized>> {
|
||||
if config_path.exists() {
|
||||
info!(
|
||||
"Using config file at default path: {}",
|
||||
@ -64,7 +60,7 @@ pub fn read_config(config_path: PathBuf) -> Result<Result<File, ConfigNotInitial
|
||||
return Ok(Err(ConfigNotInitialized {}));
|
||||
}
|
||||
|
||||
let file = File::read(&config_path)
|
||||
let file = Config::read(&config_path)
|
||||
.with_context(|| format!("failed to read config file {}", config_path.display()))?;
|
||||
|
||||
Ok(Ok(file))
|
||||
@ -72,7 +68,7 @@ pub fn read_config(config_path: PathBuf) -> Result<Result<File, ConfigNotInitial
|
||||
|
||||
pub fn initial_setup<F>(config_path: PathBuf, config_file: F) -> Result<()>
|
||||
where
|
||||
F: Fn() -> Result<File>,
|
||||
F: Fn() -> Result<Config>,
|
||||
{
|
||||
info!("Config file not found, running initial setup...");
|
||||
ensure_directory_exists(config_path.as_path())?;
|
||||
@ -88,26 +84,26 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn query_user_for_initial_testnet_config() -> Result<File> {
|
||||
pub fn query_user_for_initial_testnet_config() -> Result<Config> {
|
||||
println!();
|
||||
let bitcoind_url: String = Input::with_theme(&ColorfulTheme::default())
|
||||
let bitcoind_url = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter Bitcoind URL (including username and password if applicable) or hit return to use default")
|
||||
.default(DEFAULT_BITCOIND_TESTNET_URL.to_owned())
|
||||
.interact_text()?;
|
||||
let bitcoind_url = Url::parse(bitcoind_url.as_str())?;
|
||||
let bitcoind_url = bitcoind_url.as_str().parse()?;
|
||||
|
||||
let bitcoin_wallet_name: String = Input::with_theme(&ColorfulTheme::default())
|
||||
let bitcoin_wallet_name = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter Bitcoind wallet name")
|
||||
.interact_text()?;
|
||||
|
||||
let monero_wallet_rpc_url: String = Input::with_theme(&ColorfulTheme::default())
|
||||
let monero_wallet_rpc_url = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter Monero Wallet RPC URL or hit enter to use default")
|
||||
.default(DEFAULT_MONERO_WALLET_RPC_TESTNET_URL.to_owned())
|
||||
.interact_text()?;
|
||||
let monero_wallet_rpc_url = Url::parse(monero_wallet_rpc_url.as_str())?;
|
||||
let monero_wallet_rpc_url = monero_wallet_rpc_url.as_str().parse()?;
|
||||
println!();
|
||||
|
||||
Ok(File {
|
||||
Ok(Config {
|
||||
bitcoin: Bitcoin {
|
||||
bitcoind_url,
|
||||
wallet_name: bitcoin_wallet_name,
|
||||
@ -129,7 +125,7 @@ mod tests {
|
||||
let temp_dir = tempdir().unwrap().path().to_path_buf();
|
||||
let config_path = Path::join(&temp_dir, "config.toml");
|
||||
|
||||
let expected = File {
|
||||
let expected = Config {
|
||||
bitcoin: Bitcoin {
|
||||
bitcoind_url: Url::from_str("http://127.0.0.1:18332").unwrap(),
|
||||
wallet_name: "alice".to_string(),
|
@ -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);
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@
|
||||
|
||||
pub mod bitcoin;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod execution_params;
|
||||
pub mod fs;
|
||||
|
154
swap/src/seed.rs
154
swap/src/seed.rs
@ -1,6 +1,14 @@
|
||||
use crate::fs::ensure_directory_exists;
|
||||
use ::bitcoin::secp256k1::{self, constants::SECRET_KEY_SIZE, SecretKey};
|
||||
use pem::{encode, Pem};
|
||||
use rand::prelude::*;
|
||||
use std::fmt;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
fs::{self, File},
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub const SEED_LENGTH: usize = 32;
|
||||
|
||||
@ -21,6 +29,65 @@ impl Seed {
|
||||
pub fn bytes(&self) -> [u8; SEED_LENGTH] {
|
||||
self.0
|
||||
}
|
||||
|
||||
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.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 {
|
||||
@ -41,18 +108,101 @@ impl From<[u8; SEED_LENGTH]> for Seed {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Secp256k1: ")]
|
||||
Secp256k1(#[from] secp256k1::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 generate_random_seed() {
|
||||
let _ = Seed::random().unwrap();
|
||||
}
|
||||
|
||||
#[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.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
Block a user