commit b98f76e7a1acb6659dfdd5bf1fd543cacf0cf26b
parent 4f4640dfd5a6e6c3b5c4466d6b0a78bc04aceba7
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Wed, 22 Jul 2026 14:08:59 +0200
Improve setup seed verification feedback
Diffstat:
4 files changed, 75 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
@@ -12,6 +12,8 @@ cargo run -- --http 127.0.0.1:18661 --p2p 127.0.0.1:9444
Open `http://127.0.0.1:18661` and complete the initial setup modal. The setup flow lets you generate a local recovery phrase or import one, verifies generated phrases with a 4-word check, stores the wallet in `.mivora/wallet.json`, stores peers and setup state in `.mivora/config.json`, and does not create a chain yet.
+For fast local development, set `MIVORA_DEV_SKIP_SEED_VERIFY=1` before starting the node to show a setup-only skip button for the recovery phrase check.
+
After setup, restart with genesis mode:
```sh
diff --git a/assets/mivora-ui.js b/assets/mivora-ui.js
@@ -9,7 +9,7 @@ window.mivoraApp = function mivoraApp() {
mempool: [],
peers: [],
config: { setup_complete: false },
- setupWallet: { address: null, seed_phrase: null },
+ setupWallet: { address: null, seed_phrase: null, dev_verify_bypass: false },
setupWalletMode: "create",
setupSeedStep: "write",
generatedSeedPhrase: "",
@@ -17,6 +17,7 @@ window.mivoraApp = function mivoraApp() {
verifyAnswers: {},
importSeedPhrase: "",
walletVerified: false,
+ setupFeedback: null,
burnAmount: 0,
transferTo: "",
transferAmount: 25,
@@ -109,10 +110,12 @@ window.mivoraApp = function mivoraApp() {
selectSetupWalletMode(mode) {
this.setupWalletMode = mode;
this.walletVerified = mode === "import" ? this.walletVerified && !this.generatedSeedPhrase : false;
+ this.setupFeedback = null;
},
async generateSetupSeed() {
try {
+ this.setupFeedback = null;
const payload = await this.postWalletSetup("/api/wallet/generate", {});
this.setupWallet = payload;
this.generatedSeedPhrase = payload.seed_phrase || "";
@@ -123,14 +126,15 @@ window.mivoraApp = function mivoraApp() {
this.verifyAnswers = {};
await this.refresh();
} catch (error) {
- this.showFlash(error.message, "error");
+ this.showSetupFeedback(error.message, "error");
}
},
beginSeedVerification() {
+ this.setupFeedback = null;
const words = this.setupSeedWords();
if (words.length < 4) {
- this.showFlash("Generate a recovery phrase first", "error");
+ this.showSetupFeedback("Generate a recovery phrase first", "error");
return;
}
const positions = words.map((_, index) => index);
@@ -157,16 +161,24 @@ window.mivoraApp = function mivoraApp() {
return actual === expected;
});
if (!ok) {
- this.showFlash("Seed word check failed", "error");
+ this.showSetupFeedback("Seed word check failed", "error");
return;
}
this.walletVerified = true;
this.setupSeedStep = "verified";
- this.showFlash("Recovery phrase verified", "success");
+ this.showSetupFeedback("Recovery phrase verified", "success");
+ },
+
+ skipSeedVerificationForDev() {
+ if (!this.setupWallet.dev_verify_bypass) return;
+ this.walletVerified = true;
+ this.setupSeedStep = "verified";
+ this.showSetupFeedback("Recovery phrase verification skipped", "success");
},
async importSetupSeed() {
try {
+ this.setupFeedback = null;
const payload = await this.postWalletSetup("/api/wallet/import", {
seed_phrase: this.importSeedPhrase,
});
@@ -177,9 +189,9 @@ window.mivoraApp = function mivoraApp() {
this.walletVerified = true;
this.setupSeedStep = "verified";
await this.refresh();
- this.showFlash("Recovery phrase imported", "success");
+ this.showSetupFeedback("Recovery phrase imported", "success");
} catch (error) {
- this.showFlash(error.message, "error");
+ this.showSetupFeedback(error.message, "error");
}
},
@@ -218,6 +230,7 @@ window.mivoraApp = function mivoraApp() {
throw new Error(payload.error || `/api/config returned ${response.status}`);
}
await this.refreshConfig();
+ this.setupFeedback = null;
this.generatedSeedPhrase = "";
this.importSeedPhrase = "";
this.verifyChallenges = [];
@@ -225,7 +238,7 @@ window.mivoraApp = function mivoraApp() {
this.showFlash("Setup complete", "success");
this.setTab("wallet");
} catch (error) {
- this.showFlash(error.message, "error");
+ this.showSetupFeedback(error.message, "error");
}
},
@@ -440,6 +453,10 @@ window.mivoraApp = function mivoraApp() {
}, kind === "error" ? 7000 : 3500);
},
+ showSetupFeedback(message, kind) {
+ this.setupFeedback = { message, kind };
+ },
+
short(value) {
if (!value) return "-";
if (value.length <= 16) return value;
diff --git a/src/adapters/http.rs b/src/adapters/http.rs
@@ -74,6 +74,7 @@ struct WalletSetupResponse {
error: Option<String>,
address: Option<String>,
seed_phrase: Option<String>,
+ dev_verify_bypass: bool,
}
pub async fn serve(
@@ -289,6 +290,7 @@ async fn wallet_setup_response(state: &HttpState) -> Result<WalletSetupResponse>
error: None,
address: Some(address),
seed_phrase,
+ dev_verify_bypass: dev_seed_verify_bypass_enabled(),
})
}
@@ -305,6 +307,7 @@ async fn replace_setup_wallet_with_generated_seed(
error: None,
address: Some(address),
seed_phrase: Some(seed_phrase),
+ dev_verify_bypass: dev_seed_verify_bypass_enabled(),
})
}
@@ -321,6 +324,7 @@ async fn import_setup_wallet_seed(
error: None,
address: Some(address),
seed_phrase: None,
+ dev_verify_bypass: dev_seed_verify_bypass_enabled(),
})
}
@@ -340,10 +344,19 @@ fn wallet_setup_json(result: Result<WalletSetupResponse>) -> Json<WalletSetupRes
error: Some(format!("{error:#}")),
address: None,
seed_phrase: None,
+ dev_verify_bypass: dev_seed_verify_bypass_enabled(),
}),
}
}
+fn dev_seed_verify_bypass_enabled() -> bool {
+ dev_seed_verify_bypass_allowed(std::env::var_os("MIVORA_DEV_SKIP_SEED_VERIFY").is_some())
+}
+
+fn dev_seed_verify_bypass_allowed(env_present: bool) -> bool {
+ env_present
+}
+
async fn transfer(state: &HttpState, form: TransferForm) -> Result<()> {
let result = {
let mut node = state.node.lock().await;
@@ -444,6 +457,9 @@ const INDEX_HTML: &str = r#"<!doctype html>
.setup-modal-head h2 { margin: 0; font-size: 24px; }
.setup-welcome { color: #d5f55f; font-size: 12px; font-weight: 900; text-transform: uppercase; }
.setup-copy { max-width: 620px; color: #a8b2b8; line-height: 1.45; }
+ .setup-feedback { border: 1px solid; border-radius: 8px; padding: 10px 12px; margin-bottom: 14px; font-weight: 800; }
+ .setup-feedback.success { color: #d5f55f; background: #1c2516; border-color: #566d25; }
+ .setup-feedback.error { color: #ffb1a8; background: #2a1717; border-color: #713434; }
.setup-grid { width: 100%; display: grid; grid-template-columns: minmax(0, .9fr) minmax(320px, .7fr); gap: 12px; align-items: start; }
.setup-section { border: 1px solid #2f363c; border-radius: 8px; padding: 13px; background: #111316; }
.setup-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 14px; }
@@ -520,7 +536,7 @@ const INDEX_HTML: &str = r#"<!doctype html>
.block-card { flex-basis: 108px; }
}
</style>
- <script defer src="/assets/mivora-ui.js?v=19"></script>
+ <script defer src="/assets/mivora-ui.js?v=21"></script>
<script defer src="/assets/alpine.min.js"></script>
</head>
<body x-data="mivoraApp()" x-init="init()" x-cloak>
@@ -748,6 +764,7 @@ const INDEX_HTML: &str = r#"<!doctype html>
<h2 id="setup-title">Initial Setup</h2>
<div class="setup-copy">Confirm the local wallet address and add any peers before this node starts from a saved configuration.</div>
</div>
+ <div class="setup-feedback" :class="setupFeedback?.kind" x-show="setupFeedback" x-transition x-text="setupFeedback?.message"></div>
<div class="setup-grid">
<div class="setup-section seed-panel">
<div class="panel-head">
@@ -772,6 +789,7 @@ const INDEX_HTML: &str = r#"<!doctype html>
</div>
<div class="setup-actions">
<button type="button" class="subtle" @click="generateSetupSeed">Regenerate</button>
+ <button type="button" class="subtle" x-show="setupWallet.dev_verify_bypass" @click="skipSeedVerificationForDev">Skip verification</button>
<button type="button" class="primary" @click="beginSeedVerification">I wrote it down</button>
</div>
</div>
@@ -787,7 +805,7 @@ const INDEX_HTML: &str = r#"<!doctype html>
<div class="verify-grid">
<template x-for="challenge in verifyChallenges" :key="challenge.index">
<label>
- Word <span x-text="challenge.position"></span>
+ <span>Word <span x-text="challenge.position"></span></span>
<input x-model="verifyAnswers[challenge.index]" autocomplete="off">
</label>
</template>
@@ -836,3 +854,14 @@ const INDEX_HTML: &str = r#"<!doctype html>
</div>
</body>
</html>"#;
+
+#[cfg(test)]
+mod tests {
+ use super::dev_seed_verify_bypass_allowed;
+
+ #[test]
+ fn dev_seed_verify_bypass_requires_env_flag() {
+ assert!(dev_seed_verify_bypass_allowed(true));
+ assert!(!dev_seed_verify_bypass_allowed(false));
+ }
+}
diff --git a/src/main.rs b/src/main.rs
@@ -258,8 +258,11 @@ fn validate_wallet_for_mode(
}
fn print_help() {
- println!(
- "mivora\n\n\
+ println!("{}", help_text());
+}
+
+fn help_text() -> &'static str {
+ "mivora\n\n\
Usage:\n\
mivora [options]\n\
mivora --genesis [options]\n\
@@ -271,8 +274,9 @@ fn print_help() {
--http <addr:port> HTTP management UI address (default 127.0.0.1:18661)\n\
--p2p <addr:port> P2P TCP listener address (default 127.0.0.1:9444)\n\
--join <addr:port> Fetch chain snapshot from this peer before mining\n\
- --data-dir <path> Local wallet directory\n"
- );
+ --data-dir <path> Local wallet directory\n\n\
+ Environment:\n\
+ MIVORA_DEV_SKIP_SEED_VERIFY=1 Show a setup button to skip seed verification\n"
}
fn snapshot_height(snapshot: &ChainSnapshot) -> u64 {
@@ -505,14 +509,21 @@ mod tests {
use tokio::sync::Mutex;
use super::{
- ChainMode, CliOptions, extrapolate_vdf_rounds, initialize_ledger, measure_vdf_rounds,
- persist_chain_snapshot, run_chain_persistence_with_interval, validate_wallet_for_mode,
+ ChainMode, CliOptions, extrapolate_vdf_rounds, help_text, initialize_ledger,
+ measure_vdf_rounds, persist_chain_snapshot, run_chain_persistence_with_interval,
+ validate_wallet_for_mode,
};
fn parse(args: &[&str]) -> anyhow::Result<Option<CliOptions>> {
CliOptions::parse_from(args.iter().map(|arg| arg.to_string()))
}
+ #[test]
+ fn help_mentions_dev_seed_verify_bypass_env() {
+ assert!(help_text().contains("MIVORA_DEV_SKIP_SEED_VERIFY=1"));
+ assert!(help_text().contains("skip seed verification"));
+ }
+
fn ledger_with_one_spendable_coin(wallet: &Wallet) -> Ledger {
let mut genesis = BTreeMap::new();
genesis.insert(wallet.address().to_string(), 2);