iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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:
MREADME.md | 2++
Massets/mivora-ui.js | 33+++++++++++++++++++++++++--------
Msrc/adapters/http.rs | 33+++++++++++++++++++++++++++++++--
Msrc/main.rs | 23+++++++++++++++++------
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);