Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/gl-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ test = true
doc = true

[dependencies]
bip39 = { version = "2.2", features = ["rand"] }
clap = { version = "4.5", features = ["derive"] }
dirs = "6.0"
env_logger = "0.11"
Expand Down
38 changes: 37 additions & 1 deletion libs/gl-cli/src/node.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::{Error, Result};
use crate::model;
use crate::util::{self, CREDENTIALS_FILE_NAME};
use crate::util::{self, CREDENTIALS_FILE_NAME, SEED_FILE_NAME};
use clap::Subcommand;
use futures::stream::StreamExt;
use gl_client::pb::StreamLogRequest;
Expand All @@ -14,6 +14,12 @@ pub struct Config<P: AsRef<Path>> {

#[derive(Subcommand, Debug)]
pub enum Command {
/// Creates a new node
#[command(name = "init")]
Init {
#[arg(long)]
mnemonic: Option<String>,
},
/// Stream logs to stdout
Log,
/// Returns some basic node info
Expand Down Expand Up @@ -98,6 +104,7 @@ pub enum Command {

pub async fn command_handler<P: AsRef<Path>>(cmd: Command, config: Config<P>) -> Result<()> {
match cmd {
Command::Init { mnemonic } => init_handler(config, mnemonic).await,
Command::Log => log(config).await,
Command::GetInfo => getinfo_handler(config).await,
Command::Invoice {
Expand Down Expand Up @@ -191,6 +198,35 @@ pub async fn command_handler<P: AsRef<Path>>(cmd: Command, config: Config<P>) ->
}
}

async fn init_handler<P: AsRef<Path>>(config: Config<P>, mnemonic: Option<String>) -> Result<()> {
// Check if seed already exists in the configuration path
let seed_path = config.data_dir.as_ref().join(SEED_FILE_NAME);
if let Some(_) = util::read_seed(&seed_path) {
return Err(Error::custom(format!(
"Seed already exists at {}",
seed_path.to_string_lossy()
)));
} else {
std::fs::create_dir_all(config.data_dir.as_ref())
.map_err(|e| Error::custom(format!("Failed to create data directory: {e}")))?;
println!(
"Local greenlight directory created at {}",
config.data_dir.as_ref().to_string_lossy()
);
}

let message = match mnemonic {
Some(_) => "Secret seed derived from user provided mnemonic",
None => "Your recovery mnemonic is",
};
let (seed, mnemonic) = util::generate_seed(mnemonic)?;
util::write_seed(&seed_path, &seed)?;

// report after success
println!("{message}: {mnemonic}");
Ok(())
}

async fn log<P: AsRef<Path>>(config: Config<P>) -> Result<()> {
let creds_path = config.data_dir.as_ref().join(CREDENTIALS_FILE_NAME);
let creds = match util::read_credentials(&creds_path) {
Expand Down
5 changes: 3 additions & 2 deletions libs/gl-cli/src/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ async fn register_handler<P: AsRef<Path>>(
let seed_path = config.data_dir.as_ref().join(SEED_FILE_NAME);
let seed = match util::read_seed(&seed_path) {
Some(seed) => {
println!("Seed already exists at {}, usign it", seed_path.display());
println!("Seed already exists at {}, using it", seed_path.display());
seed
}
None => {
// Generate a new seed and save it.
let seed = util::generate_seed();
let (seed, mnemonic) = util::generate_seed(None)?;
println!("New seed generated from mnemonic: {mnemonic}");
util::write_seed(&seed_path, &seed)?;
println!("Seed saved to {}", seed_path.display());
seed.to_vec()
Expand Down
80 changes: 74 additions & 6 deletions libs/gl-cli/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use dirs;
use gl_client::bitcoin::secp256k1::rand::{self, RngCore};
use gl_client::credentials;
use std::path::PathBuf;
use std::{
Expand All @@ -15,11 +14,19 @@ pub const DEFAULT_GREENLIGHT_DIR: &str = "greenlight";

// -- Seed section

pub fn generate_seed() -> [u8; 32] {
let mut seed = [0u8; 32];
let mut rng = rand::thread_rng();
rng.fill_bytes(&mut seed);
seed
pub fn generate_seed(words: Option<String>) -> Result<([u8; 32], bip39::Mnemonic)> {
let mnemonic = match words {
Some(sentence) => bip39::Mnemonic::parse(sentence)?,
None => bip39::Mnemonic::generate(12)?,
};
let n = mnemonic.word_count();
if n != 12 {
return Err(UtilsError::custom(format!(
"Mnemonic contains {n} words, but 12 were expected."
)));
}
let seed: [u8; 32] = mnemonic.to_seed("")[0..32].try_into()?;
Ok((seed, mnemonic))
}

pub fn read_seed(file_path: impl AsRef<Path>) -> Option<Vec<u8>> {
Expand Down Expand Up @@ -81,6 +88,67 @@ impl AsRef<Path> for DataDir {
pub enum UtilsError {
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
MnemonicError(#[from] bip39::Error),
#[error(transparent)]
DataError(#[from] std::array::TryFromSliceError),
#[error("{0}")]
Custom(String),
}

impl UtilsError {
pub fn custom(e: impl std::fmt::Display) -> UtilsError {
UtilsError::Custom(e.to_string())
}
}

type Result<T, E = UtilsError> = core::result::Result<T, E>;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn gen_seed1() {
let (_, mnemonic) = generate_seed(None).unwrap();
assert_eq!(mnemonic.word_count(), 12);
}

#[test]
fn gen_seed2() {
let sentence = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
let expected_seed = [
0x5e, 0xb0, 0x0b, 0xbd, 0xdc, 0xf0, 0x69, 0x08, 0x48, 0x89, 0xa8, 0xab, 0x91, 0x55,
0x56, 0x81, 0x65, 0xf5, 0xc4, 0x53, 0xcc, 0xb8, 0x5e, 0x70, 0x81, 0x1a, 0xae, 0xd6,
0xf6, 0xda, 0x5f, 0xc1,
];
let (seed, mnemonic) = generate_seed(Some(sentence.clone())).unwrap();
assert_eq!(seed, expected_seed);
assert_eq!(mnemonic.to_string(), sentence);
}

#[test]
fn gen_seed3() {
// 0 words, invalid mnemonic
let result = generate_seed(Some("".to_string()));
assert!(result.is_err_and(|e| e.to_string().contains("invalid word count: 0")));

// 11 words, invalid mnemonic
let result = generate_seed(Some("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon".to_string()));
assert!(result.is_err_and(|e| e.to_string().contains("invalid word count: 11")));

// 15 words, valid mnemonic but we want 12 words
let result = generate_seed(Some("birth danger dismiss bounce ostrich museum model glory depth seed clip pitch skull carpet myself".to_string()));
assert!(result.is_err_and(|e| e
.to_string()
.contains("contains 15 words, but 12 were expected")));

// 12 words, but invalid word at the end
let result = generate_seed(Some("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon pizzzzza".to_string()));
assert!(result.is_err_and(|e| e.to_string().contains("unknown word (word 11)")));

// 12 words, but invalid checksum
let result = generate_seed(Some("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon pizza".to_string()));
assert!(result.is_err_and(|e| e.to_string().contains("invalid checksum")));
}
}
Loading