aldabra: --bootstrap-from-xprv power-user import path
Adds RootKey::from_root_xsk_bech32() / from_xprv_bytes() / to_root_xsk_bech32() so RootKey can ingest + emit the same bech32 root extended secret key shape that cardano-cli + cardano-address + the IOG node priv/wallet/<name>/root.prv file already use. HRP is strictly root_xsk — refuses acct_xsk/addr_xsk to keep the import scoped to the actual HD root. New CLI flag --bootstrap-from-xprv runs an interactive import: paste root_xsk1... bech32, prompt passphrase, encrypt, persist as root-xprv.age (parallel to mnemonic.age). Refuses to overwrite either existing key file (per Sulkta's no-delete-crypto-keys rule — caller has to move aside, not delete). Startup path now checks for either mnemonic.age OR root-xprv.age; refuses if both exist (ambiguous). Same RootKey downstream — derivation tree, signing, all of it works identically whether the key came in via mnemonic or xprv import. Test root_xprv_round_trip proves the imported xprv derives to the same address as the mnemonic-imported equivalent.
This commit is contained in:
parent
c57316a4fb
commit
2ff007ccaf
3 changed files with 275 additions and 45 deletions
|
|
@ -190,6 +190,78 @@ impl RootKey {
|
||||||
pub(crate) fn xprv(&self) -> &XPrv {
|
pub(crate) fn xprv(&self) -> &XPrv {
|
||||||
&self.xprv
|
&self.xprv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct from raw 96-byte XPrv (64-byte extended secret +
|
||||||
|
/// 32-byte chain code). Power-user import path — bypasses the
|
||||||
|
/// BIP-39 mnemonic flow entirely. Used when ingesting a key
|
||||||
|
/// generated by `cardano-cli address key-gen` or extracted from
|
||||||
|
/// a different Cardano wallet (the cnode `root.prv` file is the
|
||||||
|
/// canonical example).
|
||||||
|
///
|
||||||
|
/// Validates the byte count; the key itself is treated as
|
||||||
|
/// trusted input — this isn't a place to enforce semantic
|
||||||
|
/// correctness because any 96-byte sequence is a valid XPrv
|
||||||
|
/// from the type's perspective.
|
||||||
|
pub fn from_xprv_bytes(bytes: &[u8]) -> Result<Self, WalletError> {
|
||||||
|
if bytes.len() != XPRV_SIZE {
|
||||||
|
return Err(WalletError::Derivation(format!(
|
||||||
|
"xprv must be {XPRV_SIZE} bytes, got {}",
|
||||||
|
bytes.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let mut buf = [0u8; XPRV_SIZE];
|
||||||
|
buf.copy_from_slice(bytes);
|
||||||
|
let xprv = XPrv::from_bytes_verified(buf)
|
||||||
|
.map_err(|e| WalletError::Derivation(format!("xprv verify: {e:?}")))?;
|
||||||
|
Ok(RootKey { xprv })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct from a bech32-encoded extended secret key —
|
||||||
|
/// specifically the `root_xsk1...` shape that Cardano CLI's
|
||||||
|
/// HD wallet tooling (cardano-address, cardano-hw-cli, the
|
||||||
|
/// IOG node `priv/wallet/<name>/root.prv` file) emits.
|
||||||
|
///
|
||||||
|
/// HRP must be exactly `root_xsk` — we refuse other extended-
|
||||||
|
/// key flavours (`acct_xsk`, `addr_xsk`, etc) so callers don't
|
||||||
|
/// accidentally import a derived child as if it were the root.
|
||||||
|
/// If you actually want to import an account-level or address-
|
||||||
|
/// level key, you'd be locking yourself out of CIP-1852
|
||||||
|
/// derivation; we'd rather force the explicit conversation.
|
||||||
|
pub fn from_root_xsk_bech32(s: &str) -> Result<Self, WalletError> {
|
||||||
|
let trimmed = s.trim();
|
||||||
|
let (hrp, data, variant) = bech32::decode(trimmed)
|
||||||
|
.map_err(|e| WalletError::Derivation(format!("bech32 decode: {e}")))?;
|
||||||
|
if hrp != "root_xsk" {
|
||||||
|
return Err(WalletError::Derivation(format!(
|
||||||
|
"expected root_xsk bech32, got {hrp:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if variant != bech32::Variant::Bech32 {
|
||||||
|
return Err(WalletError::Derivation(
|
||||||
|
"expected Bech32 (not Bech32m) for root_xsk".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
use bech32::FromBase32;
|
||||||
|
let bytes = Vec::<u8>::from_base32(&data)
|
||||||
|
.map_err(|e| WalletError::Derivation(format!("bech32 base32: {e}")))?;
|
||||||
|
Self::from_xprv_bytes(&bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode the underlying XPrv bytes as `root_xsk1...` bech32.
|
||||||
|
/// Symmetric counterpart to [`from_root_xsk_bech32`]. Useful for
|
||||||
|
/// emitting the same shape that cardano-cli / cardano-address /
|
||||||
|
/// the IOG node's `priv/wallet/<name>/root.prv` files store.
|
||||||
|
/// **Sensitive output** — anyone with this string can spend the
|
||||||
|
/// wallet's funds.
|
||||||
|
pub fn to_root_xsk_bech32(&self) -> Result<String, WalletError> {
|
||||||
|
use bech32::ToBase32;
|
||||||
|
bech32::encode(
|
||||||
|
"root_xsk",
|
||||||
|
self.xprv.as_ref().to_base32(),
|
||||||
|
bech32::Variant::Bech32,
|
||||||
|
)
|
||||||
|
.map_err(|e| WalletError::Derivation(format!("bech32 encode: {e}")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Network parameter — bech32 prefix + protocol magic.
|
/// Network parameter — bech32 prefix + protocol magic.
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,14 @@ fn unlock_passphrase() -> Result<Zeroizing<String>> {
|
||||||
|
|
||||||
const MNEMONIC_FILENAME: &str = "mnemonic.age";
|
const MNEMONIC_FILENAME: &str = "mnemonic.age";
|
||||||
|
|
||||||
|
/// Encrypted root extended private key (BIP-32-Ed25519) — the import
|
||||||
|
/// shape for keys that came from outside the BIP-39 mnemonic flow
|
||||||
|
/// (cardano-cli `priv/wallet/<name>/root.prv`, cardano-address,
|
||||||
|
/// cardano-hw-cli, etc). Stored at the same data dir as
|
||||||
|
/// `mnemonic.age`; only one of the two should ever exist for a
|
||||||
|
/// given wallet.
|
||||||
|
const ROOT_XPRV_FILENAME: &str = "root-xprv.age";
|
||||||
|
|
||||||
/// Encrypt a mnemonic phrase with a passphrase. Pure — no I/O, no
|
/// Encrypt a mnemonic phrase with a passphrase. Pure — no I/O, no
|
||||||
/// prompts. Used by the interactive bootstrap and exposed for tests.
|
/// prompts. Used by the interactive bootstrap and exposed for tests.
|
||||||
pub fn encrypt_mnemonic(phrase: &str, passphrase: &str) -> Result<Vec<u8>> {
|
pub fn encrypt_mnemonic(phrase: &str, passphrase: &str) -> Result<Vec<u8>> {
|
||||||
|
|
@ -129,48 +137,134 @@ pub fn mnemonic_path(data_dir: &Path) -> std::path::PathBuf {
|
||||||
data_dir.join(MNEMONIC_FILENAME)
|
data_dir.join(MNEMONIC_FILENAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interactive bootstrap. If `mnemonic.age` exists at `data_dir`,
|
/// Path of the encrypted root xprv for a given data dir.
|
||||||
/// prompt for the passphrase and unlock it. Otherwise, run the
|
pub fn root_xprv_path(data_dir: &Path) -> std::path::PathBuf {
|
||||||
/// first-run flow: prompt for phrase + passphrase, encrypt, write,
|
data_dir.join(ROOT_XPRV_FILENAME)
|
||||||
/// then derive.
|
}
|
||||||
|
|
||||||
|
/// Encrypt a `root_xsk1...` bech32 string with a passphrase. We
|
||||||
|
/// encrypt the bech32 form (not the raw 96 bytes) so the on-disk
|
||||||
|
/// payload round-trips losslessly back into the same string the
|
||||||
|
/// user pasted, which makes manual recovery + cross-tool inspection
|
||||||
|
/// less error-prone.
|
||||||
|
pub fn encrypt_root_xprv_bech32(bech32_str: &str, passphrase: &str) -> Result<Vec<u8>> {
|
||||||
|
encrypt_mnemonic(bech32_str, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt an age-encrypted root xprv blob. Returns the bech32
|
||||||
|
/// `root_xsk1...` string in `Zeroizing<String>` so it gets wiped
|
||||||
|
/// when dropped.
|
||||||
|
pub fn decrypt_root_xprv_bech32(blob: &[u8], passphrase: &str) -> Result<Zeroizing<String>> {
|
||||||
|
decrypt_mnemonic(blob, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interactive bootstrap. Checks for an existing key at `data_dir` in
|
||||||
|
/// either the BIP-39 mnemonic shape (`mnemonic.age`) or the imported-
|
||||||
|
/// xprv shape (`root-xprv.age`); whichever exists is unlocked. If
|
||||||
|
/// neither exists, falls back to the BIP-39-paste first-run flow.
|
||||||
|
///
|
||||||
|
/// Refuses to start if BOTH files are present — that's an ambiguous
|
||||||
|
/// state and the user needs to pick one.
|
||||||
///
|
///
|
||||||
/// Stderr-only output. stdout is reserved for the MCP transport.
|
/// Stderr-only output. stdout is reserved for the MCP transport.
|
||||||
pub fn load_or_create_root_key(data_dir: &Path) -> Result<RootKey> {
|
pub fn load_or_create_root_key(data_dir: &Path) -> Result<RootKey> {
|
||||||
let path = mnemonic_path(data_dir);
|
let mn_path = mnemonic_path(data_dir);
|
||||||
|
let xprv_path = root_xprv_path(data_dir);
|
||||||
|
|
||||||
if path.exists() {
|
if mn_path.exists() && xprv_path.exists() {
|
||||||
eprintln!("aldabra: unlocking mnemonic at {}", path.display());
|
return Err(anyhow!(
|
||||||
let blob = fs::read(&path).with_context(|| format!("reading {}", path.display()))?;
|
"both {} and {} exist. exactly one wallet key per data dir — \
|
||||||
|
move one aside (rename, don't delete) and re-run.",
|
||||||
|
mn_path.display(),
|
||||||
|
xprv_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if mn_path.exists() {
|
||||||
|
eprintln!("aldabra: unlocking mnemonic at {}", mn_path.display());
|
||||||
|
let blob = fs::read(&mn_path).with_context(|| format!("reading {}", mn_path.display()))?;
|
||||||
let passphrase = unlock_passphrase()?;
|
let passphrase = unlock_passphrase()?;
|
||||||
let phrase = decrypt_mnemonic(&blob, &passphrase)?;
|
let phrase = decrypt_mnemonic(&blob, &passphrase)?;
|
||||||
let mnemonic = Mnemonic::from_phrase(&phrase)?;
|
let mnemonic = Mnemonic::from_phrase(&phrase)?;
|
||||||
Ok(mnemonic.into_root_key()?)
|
return Ok(mnemonic.into_root_key()?);
|
||||||
} else {
|
|
||||||
eprintln!("aldabra: no mnemonic found at {}", path.display());
|
|
||||||
eprintln!("first-run bootstrap — this writes an encrypted mnemonic to disk.\n");
|
|
||||||
fs::create_dir_all(data_dir)
|
|
||||||
.with_context(|| format!("creating {}", data_dir.display()))?;
|
|
||||||
|
|
||||||
eprint!("paste 24-word BIP-39 mnemonic (visible) and press Enter: ");
|
|
||||||
std::io::stderr().flush().ok();
|
|
||||||
let mut phrase_buf = Zeroizing::new(String::new());
|
|
||||||
std::io::stdin()
|
|
||||||
.lock()
|
|
||||||
.read_line(&mut phrase_buf)
|
|
||||||
.context("reading mnemonic from stdin")?;
|
|
||||||
let trimmed: &str = phrase_buf.trim();
|
|
||||||
|
|
||||||
// Validate before asking for passphrase — fail fast on bad input.
|
|
||||||
Mnemonic::from_phrase(trimmed)?;
|
|
||||||
|
|
||||||
let passphrase = prompt_or_env_passphrase(true)?;
|
|
||||||
let blob = encrypt_mnemonic(trimmed, &passphrase)?;
|
|
||||||
write_owner_only(&path, &blob)?;
|
|
||||||
eprintln!("aldabra: mnemonic encrypted to {}", path.display());
|
|
||||||
|
|
||||||
let mnemonic = Mnemonic::from_phrase(trimmed)?;
|
|
||||||
Ok(mnemonic.into_root_key()?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if xprv_path.exists() {
|
||||||
|
eprintln!("aldabra: unlocking root xprv at {}", xprv_path.display());
|
||||||
|
let blob =
|
||||||
|
fs::read(&xprv_path).with_context(|| format!("reading {}", xprv_path.display()))?;
|
||||||
|
let passphrase = unlock_passphrase()?;
|
||||||
|
let bech32_str = decrypt_root_xprv_bech32(&blob, &passphrase)?;
|
||||||
|
return Ok(RootKey::from_root_xsk_bech32(&bech32_str)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("aldabra: no key found at {}", data_dir.display());
|
||||||
|
eprintln!("first-run bootstrap — this writes an encrypted mnemonic to disk.\n");
|
||||||
|
fs::create_dir_all(data_dir)
|
||||||
|
.with_context(|| format!("creating {}", data_dir.display()))?;
|
||||||
|
|
||||||
|
eprint!("paste 24-word BIP-39 mnemonic (visible) and press Enter: ");
|
||||||
|
std::io::stderr().flush().ok();
|
||||||
|
let mut phrase_buf = Zeroizing::new(String::new());
|
||||||
|
std::io::stdin()
|
||||||
|
.lock()
|
||||||
|
.read_line(&mut phrase_buf)
|
||||||
|
.context("reading mnemonic from stdin")?;
|
||||||
|
let trimmed: &str = phrase_buf.trim();
|
||||||
|
|
||||||
|
// Validate before asking for passphrase — fail fast on bad input.
|
||||||
|
Mnemonic::from_phrase(trimmed)?;
|
||||||
|
|
||||||
|
let passphrase = prompt_or_env_passphrase(true)?;
|
||||||
|
let blob = encrypt_mnemonic(trimmed, &passphrase)?;
|
||||||
|
write_owner_only(&mn_path, &blob)?;
|
||||||
|
eprintln!("aldabra: mnemonic encrypted to {}", mn_path.display());
|
||||||
|
|
||||||
|
let mnemonic = Mnemonic::from_phrase(trimmed)?;
|
||||||
|
Ok(mnemonic.into_root_key()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import a root xprv (`root_xsk1...` bech32) from stdin, encrypt
|
||||||
|
/// with a passphrase, and persist as `root-xprv.age`. Returns the
|
||||||
|
/// derived [`RootKey`]. Refuses to overwrite an existing key file
|
||||||
|
/// (mnemonic OR xprv) — caller has to move the existing one aside
|
||||||
|
/// (per the no-delete-crypto-keys policy).
|
||||||
|
pub fn import_root_xprv(data_dir: &Path) -> Result<RootKey> {
|
||||||
|
let mn_path = mnemonic_path(data_dir);
|
||||||
|
let xprv_path = root_xprv_path(data_dir);
|
||||||
|
if mn_path.exists() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"{} already exists — move it aside (rename, don't delete) before importing.",
|
||||||
|
mn_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if xprv_path.exists() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"{} already exists — move it aside (rename, don't delete) before importing.",
|
||||||
|
xprv_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
fs::create_dir_all(data_dir)
|
||||||
|
.with_context(|| format!("creating {}", data_dir.display()))?;
|
||||||
|
|
||||||
|
eprint!("paste root_xsk1... bech32 root extended secret key and press Enter: ");
|
||||||
|
std::io::stderr().flush().ok();
|
||||||
|
let mut buf = Zeroizing::new(String::new());
|
||||||
|
std::io::stdin()
|
||||||
|
.lock()
|
||||||
|
.read_line(&mut buf)
|
||||||
|
.context("reading root_xsk from stdin")?;
|
||||||
|
let trimmed: &str = buf.trim();
|
||||||
|
|
||||||
|
// Validate before asking for passphrase.
|
||||||
|
let root = RootKey::from_root_xsk_bech32(trimmed)?;
|
||||||
|
|
||||||
|
let passphrase = prompt_or_env_passphrase(true)?;
|
||||||
|
let blob = encrypt_root_xprv_bech32(trimmed, &passphrase)?;
|
||||||
|
write_owner_only(&xprv_path, &blob)?;
|
||||||
|
eprintln!("aldabra: root xprv encrypted to {}", xprv_path.display());
|
||||||
|
|
||||||
|
Ok(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print a freshly generated 24-word mnemonic to stderr and exit.
|
/// Print a freshly generated 24-word mnemonic to stderr and exit.
|
||||||
|
|
@ -278,4 +372,57 @@ mod tests {
|
||||||
let b = encrypt_mnemonic(ABANDON_ART, "hunter2").unwrap();
|
let b = encrypt_mnemonic(ABANDON_ART, "hunter2").unwrap();
|
||||||
assert_ne!(a, b);
|
assert_ne!(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Public-API derivation of the canonical abandon-art root_xsk
|
||||||
|
/// for test reuse. Goes through Mnemonic→RootKey→bech32, which
|
||||||
|
/// is the same path power users will hit from outside the crate.
|
||||||
|
fn abandon_art_root_xsk_bech32() -> String {
|
||||||
|
Mnemonic::from_phrase(ABANDON_ART)
|
||||||
|
.unwrap()
|
||||||
|
.into_root_key()
|
||||||
|
.unwrap()
|
||||||
|
.to_root_xsk_bech32()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn root_xprv_round_trip() {
|
||||||
|
let bech = abandon_art_root_xsk_bech32();
|
||||||
|
let blob = encrypt_root_xprv_bech32(&bech, "hunter2").unwrap();
|
||||||
|
let decrypted = decrypt_root_xprv_bech32(&blob, "hunter2").unwrap();
|
||||||
|
assert_eq!(&*decrypted as &str, bech.as_str());
|
||||||
|
// Imported RootKey must derive to the same address as the
|
||||||
|
// mnemonic path (proves bech32 round trip preserves entropy).
|
||||||
|
let root_a = Mnemonic::from_phrase(ABANDON_ART)
|
||||||
|
.unwrap()
|
||||||
|
.into_root_key()
|
||||||
|
.unwrap();
|
||||||
|
let addr_a = aldabra_core::derive_base_address(
|
||||||
|
&root_a,
|
||||||
|
aldabra_core::Network::Mainnet,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let root_b = RootKey::from_root_xsk_bech32(&decrypted).unwrap();
|
||||||
|
let addr_b = aldabra_core::derive_base_address(
|
||||||
|
&root_b,
|
||||||
|
aldabra_core::Network::Mainnet,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
addr_a, addr_b,
|
||||||
|
"xprv import must derive the same address as mnemonic import"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn root_xprv_rejects_garbage() {
|
||||||
|
match RootKey::from_root_xsk_bech32("clearly not bech32") {
|
||||||
|
Ok(_) => panic!("expected error"),
|
||||||
|
Err(e) => assert!(e.to_string().to_lowercase().contains("bech32")),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,14 +61,19 @@ async fn run() -> Result<()> {
|
||||||
// CLI mode flags — all out-of-band, before the MCP transport
|
// CLI mode flags — all out-of-band, before the MCP transport
|
||||||
// takes over stdio.
|
// takes over stdio.
|
||||||
//
|
//
|
||||||
// `--generate-mnemonic` — print a fresh phrase, exit. No disk write.
|
// `--generate-mnemonic` — print a fresh phrase, exit. No disk write.
|
||||||
// `--bootstrap` — paste an existing phrase, encrypt, derive.
|
// `--bootstrap` — paste an existing phrase, encrypt, derive.
|
||||||
// `--bootstrap-new` — generate, display, encrypt, derive (one shot).
|
// `--bootstrap-new` — generate, display, encrypt, derive (one shot).
|
||||||
// (none) — load existing, start MCP server.
|
// `--bootstrap-from-xprv` — paste a root_xsk1... bech32 (cnode root.prv,
|
||||||
|
// cardano-address output), encrypt, derive.
|
||||||
|
// Power-user import path for keys that came
|
||||||
|
// from outside the BIP-39 mnemonic flow.
|
||||||
|
// (none) — load existing, start MCP server.
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let generate_only = args.iter().any(|a| a == "--generate-mnemonic");
|
let generate_only = args.iter().any(|a| a == "--generate-mnemonic");
|
||||||
let bootstrap_only = args.iter().any(|a| a == "--bootstrap");
|
let bootstrap_only = args.iter().any(|a| a == "--bootstrap");
|
||||||
let bootstrap_new = args.iter().any(|a| a == "--bootstrap-new");
|
let bootstrap_new = args.iter().any(|a| a == "--bootstrap-new");
|
||||||
|
let bootstrap_from_xprv = args.iter().any(|a| a == "--bootstrap-from-xprv");
|
||||||
|
|
||||||
if generate_only {
|
if generate_only {
|
||||||
bootstrap::print_fresh_mnemonic()?;
|
bootstrap::print_fresh_mnemonic()?;
|
||||||
|
|
@ -76,22 +81,28 @@ async fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mnemonic_path = bootstrap::mnemonic_path(&cfg.data_dir);
|
let mnemonic_path = bootstrap::mnemonic_path(&cfg.data_dir);
|
||||||
|
let xprv_path = bootstrap::root_xprv_path(&cfg.data_dir);
|
||||||
|
let any_key_exists = mnemonic_path.exists() || xprv_path.exists();
|
||||||
|
|
||||||
// L-2 audit fix: scope `root` to a block so its XPrv drops + wipes
|
// L-2 audit fix: scope `root` to a block so its XPrv drops + wipes
|
||||||
// as soon as we've extracted the keys we need.
|
// as soon as we've extracted the keys we need.
|
||||||
let (payment_key, stake_key, address) = {
|
let (payment_key, stake_key, address) = {
|
||||||
let root = if bootstrap_new {
|
let root = if bootstrap_new {
|
||||||
bootstrap::generate_and_save_root_key(&cfg.data_dir)?
|
bootstrap::generate_and_save_root_key(&cfg.data_dir)?
|
||||||
} else if mnemonic_path.exists() || bootstrap_only {
|
} else if bootstrap_from_xprv {
|
||||||
// Both branches load (or create on the bootstrap path).
|
bootstrap::import_root_xprv(&cfg.data_dir)?
|
||||||
// `load_or_create_root_key` itself decides which based on
|
} else if any_key_exists || bootstrap_only {
|
||||||
// whether `mnemonic.age` exists.
|
// Loads existing (or runs the mnemonic-paste flow on
|
||||||
|
// first-run with --bootstrap). load_or_create_root_key
|
||||||
|
// itself picks between mnemonic.age and root-xprv.age.
|
||||||
bootstrap::load_or_create_root_key(&cfg.data_dir)?
|
bootstrap::load_or_create_root_key(&cfg.data_dir)?
|
||||||
} else {
|
} else {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"no mnemonic at {}. run `aldabra --bootstrap` (paste existing) \
|
"no key at {}. run one of:\n \
|
||||||
or `aldabra --bootstrap-new` (generate fresh) first.",
|
`aldabra --bootstrap` (paste existing 24-word phrase)\n \
|
||||||
mnemonic_path.display()
|
`aldabra --bootstrap-new` (generate a fresh wallet)\n \
|
||||||
|
`aldabra --bootstrap-from-xprv` (paste a root_xsk1... bech32)",
|
||||||
|
cfg.data_dir.display()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue