commit 91c7d10b41226f9f6e1063ce14f90c048559f41d
parent 629575e02c5cc98ee64b4e7dbd15b52fa61fb64f
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Mon, 24 Aug 2026 22:05:28 +0200
Add network reset migration flow
Diffstat:
16 files changed, 227 insertions(+), 38 deletions(-)
diff --git a/ROADMAP.md b/ROADMAP.md
@@ -98,7 +98,7 @@ Focus: promote the stable candidate ledger. Mainnet launch should not create a s
- [ ] Tag the mainnet release from the promoted candidate code line.
- [ ] Publish release artifacts and checksums.
- [ ] Upgrade or restart bootnodes on the mainnet release while preserving chain data.
-- [ ] If the P2P network ID changes from `iuna-mainnet-candidate-v1` to `iuna-mainnet-v1`, coordinate the cutover without changing genesis or launch profile rules.
+- [ ] If the P2P network ID changes from `iuna-mainnet-candidate` to `iuna-mainnet-v1`, coordinate the cutover without changing genesis or launch profile rules.
- [ ] Monitor first blocks and first recovery/fallback events.
- [ ] Keep feature changes frozen during the launch window.
- [ ] Document any required hard-fork or emergency procedure before launch.
diff --git a/docs/genesis.md b/docs/genesis.md
@@ -230,7 +230,7 @@ If the candidate passes the stability window, publish a promotion notice instead
- promoted tip height and tip hash;
- final candidate release tag and mainnet release tag;
- bootnodes that will remain online through the cutover;
-- whether the P2P network ID changes from `iuna-mainnet-candidate-v1` to `iuna-mainnet-v1`;
+- whether the P2P network ID changes from `iuna-mainnet-candidate` to `iuna-mainnet-v1`;
- the exact upgrade window for operators.
Do not delete chain data when promoting. Nodes should keep `chain.sqlite3`, wallet files, and UI data, then upgrade or restart with the promoted release. A P2P network ID change fences upgraded mainnet nodes away from old candidate binaries, but it must not change genesis, launch profile rules, or any existing ledger state.
diff --git a/docs/protocol.md b/docs/protocol.md
@@ -16,10 +16,10 @@ This is still experimental. The rules below describe the current devnet and main
The current mainnet-candidate parameter set is intentionally close to Bitcoin where that is useful for operator expectations:
-- P2P network ID: `iuna-mainnet-candidate-v1`;
+- P2P network ID: `iuna-mainnet-candidate`;
- protocol version: `2`;
-- launch profile ID: `iuna-mainnet-candidate-v1`;
-- launch profile hash: `aef51531eaa3a5c5d3ea8a2524ffba029dcb106e4b0a432b57d5ac1f4f8963de`;
+- launch profile ID: `iuna-mainnet-candidate`;
+- launch profile hash: `eb2f67e9d735474859ceb1fe124fe270977214f6e2f4cd855a4d8c3b5ecac558`;
- target block time: `10 minutes`;
- maximum serialized block size: `1,000,000` bytes;
- maximum transaction items per block: `1,000`;
diff --git a/fuzz/corpus/p2p_envelope/hello.json b/fuzz/corpus/p2p_envelope/hello.json
@@ -1 +1 @@
-{"type":"hello","protocol_version":1,"network_id":"iuna-mainnet-candidate-v1","genesis_hash":"0000000000000000000000000000000000000000000000000000000000000000","listen_addr":null,"node_id":null,"height":0,"tip_hash":"0000000000000000000000000000000000000000000000000000000000000000","time_ms":0}
+{"type":"hello","protocol_version":1,"network_id":"iuna-mainnet-candidate","genesis_hash":"0000000000000000000000000000000000000000000000000000000000000000","listen_addr":null,"node_id":null,"height":0,"tip_hash":"0000000000000000000000000000000000000000000000000000000000000000","time_ms":0}
diff --git a/src/adapters/http/actions.rs b/src/adapters/http/actions.rs
@@ -325,6 +325,7 @@ pub(super) async fn reset_local_chain(state: &HttpState, confirmation: &str) ->
}
clear_chain(&state.chain_store).await?;
clear_ui_data(&state.ui_data_store).await?;
+ state.node.lock().await.complete_network_migration();
state
.gossip
.broadcast(vec![GossipEnvelope::ChainBootstrapRequest])
@@ -563,7 +564,9 @@ mod tests {
chain_store.save(&snapshot).unwrap();
ui_data_store.project_snapshot(&snapshot, true).unwrap();
- let node = Arc::new(Mutex::new(NodeCore::from_ledger(wallet, ledger, 0)));
+ let mut node_core = NodeCore::from_ledger(wallet, ledger, 0);
+ node_core.require_network_migration("iuna-devnet-v5");
+ let node = Arc::new(Mutex::new(node_core));
let peers = Arc::new(Mutex::new(PeerBook::default()));
let gossip = GossipNetwork::start(node.clone(), peers.clone(), socket(), None, false)
.await
@@ -590,6 +593,7 @@ mod tests {
.unwrap();
assert!(!state.node.lock().await.has_real_chain());
+ assert!(state.node.lock().await.network_migration_from().is_none());
assert!(chain_store.load().unwrap().is_none());
assert!(ui_data_store.load_metrics().unwrap().is_empty());
assert_eq!(std::fs::read(&wallet_path).unwrap(), wallet_before);
diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs
@@ -1280,6 +1280,24 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
</section>
</main>
</div>
+ <div class="setup-overlay" x-show="showingNetworkMigration()" x-transition.opacity role="dialog" aria-modal="true" aria-labelledby="network-migration-title">
+ <section class="setup-modal">
+ <div class="setup-modal-head">
+ <div class="setup-welcome">A new Iuna network is ready 🎉</div>
+ <h2 id="network-migration-title">Start from block 0</h2>
+ <div class="setup-copy">This version connects to <strong x-text="migrationNetworkLabel()"></strong>. To join it, this node must remove its previous local blockchain and synchronize again.</div>
+ </div>
+ <div class="panel">
+ <h3>Your wallet is separate</h3>
+ <p class="muted">Keeping your current wallet preserves its address and recovery phrase. Removing the local chain does not remove your wallet or settings.</p>
+ <div class="setup-actions">
+ <button class="primary" type="button" :disabled="migrationBusy" @click="finishNetworkMigration(false)" x-text="migrationBusy ? 'Preparing...' : 'Keep wallet and resync'"></button>
+ <button class="subtle" type="button" :disabled="migrationBusy" @click="finishNetworkMigration(true)">Use a different wallet</button>
+ </div>
+ <p class="danger-copy">Choose a different wallet only if you want a new address or need to import another recovery phrase. Without the old recovery phrase, its funds cannot be recovered on your new address.</p>
+ </div>
+ </section>
+ </div>
<div class="sync-overlay" x-show="syncingNode()" role="status" aria-live="polite" aria-label="Blockchain synchronization in progress">
<section class="sync-screen">
<div class="sync-mark" aria-hidden="true"><div class="sync-spinner"></div></div>
@@ -1289,6 +1307,8 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
<div class="sync-progress-fill" :style="`width: ${syncProgressPercent()}%`"></div>
</div>
<div class="sync-progress-label" x-text="syncProgressLabel()"></div>
+ <button class="subtle" type="button" @click="openChainResetModal">Sync stuck? Reset local chain</button>
+ <p class="muted">This recovery option removes only the local blockchain. Your wallet and settings stay on this device.</p>
</section>
</div>
<div class="setup-overlay transaction-overlay" x-show="chainResetModalOpen" x-transition.opacity @click.self="closeChainResetModal()" role="dialog" aria-modal="true" aria-labelledby="chain-reset-title">
diff --git a/src/adapters/http/wallet.rs b/src/adapters/http/wallet.rs
@@ -26,7 +26,8 @@ pub(super) async fn wallet_setup_response(
) -> Result<WalletSetupResponse> {
let setup_complete = state.ui_config.lock().await.setup_complete;
let password = wallet_password_for_request(state, headers).await;
- let seed_phrase = if setup_complete {
+ let migration_required = state.node.lock().await.network_migration_from().is_some();
+ let seed_phrase = if setup_complete && !migration_required {
None
} else {
wallet_store::setup_seed_phrase_with_password(&state.wallet_path, password.as_deref())?
@@ -96,7 +97,8 @@ pub(super) async fn import_setup_wallet_seed(
async fn ensure_wallet_setup_open(state: &HttpState) -> Result<()> {
let setup_complete = state.ui_config.lock().await.setup_complete;
- if setup_complete {
+ let migration_required = state.node.lock().await.network_migration_from().is_some();
+ if setup_complete && !migration_required {
bail!("wallet setup is already complete");
}
Ok(())
diff --git a/src/app.rs b/src/app.rs
@@ -29,8 +29,8 @@ pub use in_memory_network::InMemoryNetwork;
pub use peer_book::{PeerBook, PeerDirection, PeerInfo};
pub use types::{
AutoMineOutcome, AutoMinePlan, BlockInventory, ChainBootstrap, ExternalMineJob, FeeEstimate,
- GossipEnvelope, LaunchProfileStatus, MiningStatus, NodeConfig, NodeStatus, ProtocolHello,
- StratumStatus,
+ GossipEnvelope, LaunchProfileStatus, MiningStatus, NetworkMigrationStatus, NodeConfig,
+ NodeStatus, ProtocolHello, StratumStatus,
};
use wallet::NodeWallet;
@@ -40,7 +40,7 @@ pub type SharedPeerBook = Arc<Mutex<PeerBook>>;
pub const DEFAULT_BURN_PER_BLOCK: Amount = 0;
pub const DEFAULT_VDF_ROUNDS: u32 = 67_000_000;
pub const PROTOCOL_VERSION: u32 = 2;
-pub const MAINNET_CANDIDATE_NETWORK_ID: &str = "iuna-mainnet-candidate-v1";
+pub const MAINNET_CANDIDATE_NETWORK_ID: &str = "iuna-mainnet-candidate";
pub const MAINNET_NETWORK_ID: &str = "iuna-mainnet-v1";
pub const NETWORK_ID: &str = MAINNET_CANDIDATE_NETWORK_ID;
pub const BLOCK_REQUEST_LIMIT: usize = 128;
@@ -68,7 +68,7 @@ mod tests {
fn mainnet_candidate_network_parameters_are_frozen() {
assert_eq!(DEFAULT_VDF_ROUNDS, 67_000_000);
assert_eq!(PROTOCOL_VERSION, 2);
- assert_eq!(MAINNET_CANDIDATE_NETWORK_ID, "iuna-mainnet-candidate-v1");
+ assert_eq!(MAINNET_CANDIDATE_NETWORK_ID, "iuna-mainnet-candidate");
assert_eq!(MAINNET_NETWORK_ID, "iuna-mainnet-v1");
assert_ne!(MAINNET_CANDIDATE_NETWORK_ID, MAINNET_NETWORK_ID);
assert_eq!(NETWORK_ID, MAINNET_CANDIDATE_NETWORK_ID);
@@ -140,6 +140,7 @@ pub struct NodeCore {
burn_bundle_collection_started: Option<(u64, u64)>,
local_block_anchor_burn: Option<(u64, Transaction)>,
outbox: Vec<GossipEnvelope>,
+ network_migration_from: Option<String>,
}
pub fn now_ms() -> u64 {
diff --git a/src/app/node_lifecycle.rs b/src/app/node_lifecycle.rs
@@ -107,9 +107,22 @@ impl NodeCore {
burn_bundle_collection_started: None,
local_block_anchor_burn: None,
outbox: Vec::<GossipEnvelope>::new(),
+ network_migration_from: None,
}
}
+ pub fn require_network_migration(&mut self, from_network: impl Into<String>) {
+ self.network_migration_from = Some(from_network.into());
+ }
+
+ pub fn network_migration_from(&self) -> Option<&str> {
+ self.network_migration_from.as_deref()
+ }
+
+ pub fn complete_network_migration(&mut self) {
+ self.network_migration_from = None;
+ }
+
pub fn wallet_address(&self) -> &str {
self.wallet.address()
}
diff --git a/src/app/receive.rs b/src/app/receive.rs
@@ -143,6 +143,9 @@ impl NodeCore {
}
pub fn import_chain_snapshot(&mut self, snapshot: ChainSnapshot) -> Result<()> {
+ if self.network_migration_from().is_some() {
+ anyhow::bail!("local chain reset is required before joining the new network");
+ }
let previous_height = self.ledger.height();
let imported = self.ledger.extend_from_snapshot(snapshot)?;
if imported {
@@ -156,6 +159,9 @@ impl NodeCore {
}
pub(crate) fn import_verified_ledger(&mut self, ledger: Ledger) -> Result<bool> {
+ if self.network_migration_from().is_some() {
+ anyhow::bail!("local chain reset is required before joining the new network");
+ }
let replaces_setup_placeholder = self.ledger.is_setup_placeholder()
&& ledger.genesis_hash() != self.ledger.genesis_hash();
if ledger.genesis_hash() != self.ledger.genesis_hash() && !replaces_setup_placeholder {
diff --git a/src/app/status.rs b/src/app/status.rs
@@ -6,7 +6,8 @@ use crate::{
};
use super::{
- LaunchProfileStatus, MiningStatus, NodeCore, NodeStatus, StratumStatus,
+ LaunchProfileStatus, MiningStatus, NETWORK_ID, NetworkMigrationStatus, NodeCore, NodeStatus,
+ StratumStatus,
helpers::{transaction_input_total_from_outputs, transaction_output_total_for_address},
now_ms,
};
@@ -60,6 +61,11 @@ impl NodeCore {
listen_addr: None,
},
chain,
+ network_migration: NetworkMigrationStatus {
+ required: self.network_migration_from().is_some(),
+ from_network: self.network_migration_from().map(str::to_string),
+ to_network: NETWORK_ID.to_string(),
+ },
}
}
@@ -212,4 +218,21 @@ mod tests {
Some("waiting for a real chain before PoW mining can start")
);
}
+
+ #[test]
+ fn status_exposes_required_network_migration() {
+ let wallet = Wallet::from_seed("network-migration-status-wallet");
+ let ledger = Ledger::new(BTreeMap::new(), 1);
+ let mut node = NodeCore::from_ledger(wallet, ledger, 0);
+
+ node.require_network_migration("iuna-devnet-v5");
+ let status = node.status();
+
+ assert!(status.network_migration.required);
+ assert_eq!(
+ status.network_migration.from_network.as_deref(),
+ Some("iuna-devnet-v5")
+ );
+ assert_eq!(status.network_migration.to_network, crate::app::NETWORK_ID);
+ }
}
diff --git a/src/app/types.rs b/src/app/types.rs
@@ -132,6 +132,14 @@ pub struct NodeStatus {
pub mining: MiningStatus,
pub stratum: StratumStatus,
pub chain: ChainStatus,
+ pub network_migration: NetworkMigrationStatus,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct NetworkMigrationStatus {
+ pub required: bool,
+ pub from_network: Option<String>,
+ pub to_network: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
diff --git a/src/domain/profile.rs b/src/domain/profile.rs
@@ -25,7 +25,7 @@ pub struct LaunchProfile {
impl Default for LaunchProfile {
fn default() -> Self {
Self {
- profile_id: "iuna-mainnet-candidate-v1".to_string(),
+ profile_id: "iuna-mainnet-candidate".to_string(),
ticket_maturity_delay_heights: DEFAULT_TICKET_MATURITY_DELAY,
ticket_expiry_window_heights: DEFAULT_TICKET_EXPIRY_WINDOW,
mine_difficulty_bits: MINE_DIFFICULTY_BITS,
@@ -64,9 +64,8 @@ impl LaunchProfile {
pub fn hash(&self) -> String {
let canonical = if self.burn_lineage_maturity_heights == BURN_LINEAGE_MATURITY_HEIGHTS {
- // Preserve the existing mainnet-candidate profile hash. The maturity
- // used to be a frozen protocol constant, so including its unchanged
- // default would needlessly split the existing network.
+ // Preserve the legacy canonical format when maturity remains at its
+ // protocol default. The field used to be a frozen constant.
format!(
"iuna-launch-profile:{}:{}:{}:{}:{}:{}:{}",
self.profile_id,
@@ -121,7 +120,7 @@ mod tests {
fn default_launch_profile_matches_protocol_defaults() {
let profile = LaunchProfile::default();
- assert_eq!(profile.profile_id, "iuna-mainnet-candidate-v1");
+ assert_eq!(profile.profile_id, "iuna-mainnet-candidate");
assert_eq!(
profile.ticket_maturity_delay_heights,
DEFAULT_TICKET_MATURITY_DELAY
@@ -140,7 +139,7 @@ mod tests {
assert_eq!(profile.max_block_bytes, MAX_BLOCK_BYTES);
assert_eq!(
profile.hash(),
- "aef51531eaa3a5c5d3ea8a2524ffba029dcb106e4b0a432b57d5ac1f4f8963de"
+ "eb2f67e9d735474859ceb1fe124fe270977214f6e2f4cd855a4d8c3b5ecac558"
);
}
diff --git a/src/main.rs b/src/main.rs
@@ -121,7 +121,7 @@ async fn main() -> Result<()> {
if ui_config_dirty || auth_config_dirty {
config_store::save(&config_path, &ui_config)?;
}
- let ledger = initialize_ledger(
+ let initialized_ledger = initialize_ledger(
&opts,
&wallet_address,
&chain_store,
@@ -129,7 +129,10 @@ async fn main() -> Result<()> {
startup_local_testnet,
)
.await?;
- let has_chain = opts.has_chain() || persisted_chain_exists;
+ let migration_from = initialized_ledger.migration_from.clone();
+ let migration_required = migration_from.is_some();
+ let ledger = initialized_ledger.ledger;
+ let has_chain = migration_from.is_none() && (opts.has_chain() || persisted_chain_exists);
let initial_burn_per_block = initial_burn_per_block(&opts, &ui_config);
let initial_burn_fee = initial_burn_fee(&opts, &ui_config);
@@ -152,6 +155,13 @@ async fn main() -> Result<()> {
node_core.set_pow_mining_workers(ui_config.pow_mining_workers);
node_core.set_pow_mining_enabled(ui_config.pow_mining_enabled);
node_core.set_recovery_vdf_top_rank_percent(ui_config.recovery_vdf_top_rank_percent);
+ if let Some(from_network) = migration_from {
+ println!(
+ "network upgrade requires local chain reset: {from_network} -> {}",
+ iuna::app::NETWORK_ID
+ );
+ node_core.require_network_migration(from_network);
+ }
let node: SharedNode = Arc::new(Mutex::new(node_core));
let ui_config = Arc::new(Mutex::new(ui_config));
let mut peers = ui_config.lock().await.peers.clone();
@@ -162,7 +172,7 @@ async fn main() -> Result<()> {
let keep_metrics = ui_config.lock().await.keep_track_of_metrics;
persist_chain_snapshot(&chain_store, initial_snapshot.clone()).await?;
warm_ui_data_store(&ui_data_store, initial_snapshot, keep_metrics).await?;
- } else {
+ } else if !migration_required {
clear_ui_data_store(&ui_data_store).await?;
}
@@ -475,7 +485,7 @@ async fn initialize_ledger(
chain_store: &SqliteChainStore,
advertised_p2p_addr: SocketAddr,
local_testnet: bool,
-) -> Result<Ledger> {
+) -> Result<InitializedLedger> {
if let Some(snapshot) = chain_store.load()? {
if opts.chain_mode == ChainMode::Genesis {
bail!(
@@ -483,10 +493,16 @@ async fn initialize_ledger(
chain_store.path().display()
);
}
- if local_testnet && snapshot.launch_profile != LaunchProfile::local_testnet() {
- bail!(
- "{LOCAL_TESTNET_ENV}=true requires the iuna-local-testnet-v1 launch profile; reset this local chain before restarting"
- );
+ let expected_profile = if local_testnet {
+ LaunchProfile::local_testnet()
+ } else {
+ LaunchProfile::default()
+ };
+ if snapshot.launch_profile.profile_id != expected_profile.profile_id {
+ return Ok(InitializedLedger {
+ ledger: setup_ledger(),
+ migration_from: Some(snapshot.launch_profile.profile_id),
+ });
}
let height = snapshot_height(&snapshot);
let ledger = Ledger::from_persisted_snapshot(snapshot).with_context(|| {
@@ -499,13 +515,40 @@ async fn initialize_ledger(
"resumed chain from {} at height {height}",
chain_store.path().display()
);
- Ok(ledger)
+ Ok(InitializedLedger {
+ ledger,
+ migration_from: None,
+ })
} else {
- match opts.chain_mode {
+ let ledger = match opts.chain_mode {
ChainMode::Setup => Ok(setup_ledger()),
ChainMode::Genesis => start_genesis_ledger(wallet_address, local_testnet),
ChainMode::Join => join_chain_ledger(&opts.join_peers, advertised_p2p_addr).await,
- }
+ }?;
+ Ok(InitializedLedger {
+ ledger,
+ migration_from: None,
+ })
+ }
+}
+
+#[derive(Debug)]
+struct InitializedLedger {
+ ledger: Ledger,
+ migration_from: Option<String>,
+}
+
+impl std::ops::Deref for InitializedLedger {
+ type Target = Ledger;
+
+ fn deref(&self) -> &Self::Target {
+ &self.ledger
+ }
+}
+
+impl std::ops::DerefMut for InitializedLedger {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.ledger
}
}
diff --git a/src/main_tests.rs b/src/main_tests.rs
@@ -89,6 +89,10 @@ fn management_ui_blocks_interaction_while_the_node_is_syncing() {
assert!(javascript.contains("this.networkHealth.sync_validated_height"));
assert!(javascript.contains("this.syncingNode() ? 1000 : 5000"));
assert!(javascript.contains("Syncing ${this.syncCurrentHeight().toLocaleString()} of"));
+ assert!(javascript.contains("!this.showingNetworkMigration()"));
+ assert!(javascript.contains("!this.chainResetModalOpen"));
+ assert!(html.contains("Sync stuck? Reset local chain"));
+ assert!(html.contains("@click=\"openChainResetModal\""));
}
fn ledger_with_one_spendable_iuna(wallet: &Wallet) -> Ledger {
@@ -900,7 +904,7 @@ async fn startup_resumes_persisted_chain_without_genesis_flag() {
}
#[tokio::test]
-async fn local_testnet_refuses_persisted_chain_with_normal_launch_profile() {
+async fn local_testnet_requests_reset_for_persisted_normal_launch_profile() {
let dir = tempdir().unwrap();
let chain_path = dir.path().join("chain.sqlite3");
let store = SqliteChainStore::open(&chain_path).unwrap();
@@ -911,7 +915,7 @@ async fn local_testnet_refuses_persisted_chain_with_normal_launch_profile() {
.unwrap()
.unwrap();
- let error = initialize_ledger(
+ let initialized = initialize_ledger(
&opts,
persisted_wallet.address(),
&store,
@@ -919,14 +923,46 @@ async fn local_testnet_refuses_persisted_chain_with_normal_launch_profile() {
true,
)
.await
- .unwrap_err();
+ .unwrap();
+
+ assert_eq!(
+ initialized.migration_from.as_deref(),
+ Some("iuna-mainnet-candidate")
+ );
+ assert!(initialized.ledger.is_setup_placeholder());
+}
- assert!(error.to_string().contains("reset this local chain"));
+#[tokio::test]
+async fn startup_requests_reset_for_a_legacy_network_profile() {
+ let dir = tempdir().unwrap();
+ let chain_path = dir.path().join("chain.sqlite3");
+ let store = SqliteChainStore::open(&chain_path).unwrap();
+ let wallet = Wallet::from_seed("legacy-network-owner");
+ let mut snapshot = ledger_with_one_mined_block(&wallet).snapshot();
+ snapshot.launch_profile.profile_id = "iuna-devnet-v5".to_string();
+ store.save(&snapshot).unwrap();
+ let opts = parse(&["--chain-db", chain_path.to_str().unwrap()])
+ .unwrap()
+ .unwrap();
+
+ let initialized = initialize_ledger(&opts, wallet.address(), &store, opts.p2p_addr, false)
+ .await
+ .unwrap();
+
+ assert_eq!(
+ initialized.migration_from.as_deref(),
+ Some("iuna-devnet-v5")
+ );
+ assert!(initialized.ledger.is_setup_placeholder());
+ assert_eq!(
+ store.load().unwrap().unwrap().launch_profile.profile_id,
+ "iuna-devnet-v5"
+ );
}
#[tokio::test]
async fn candidate_promotion_reuses_chain_data_and_can_continue_mining() {
- assert_eq!(MAINNET_CANDIDATE_NETWORK_ID, "iuna-mainnet-candidate-v1");
+ assert_eq!(MAINNET_CANDIDATE_NETWORK_ID, "iuna-mainnet-candidate");
assert_eq!(MAINNET_NETWORK_ID, "iuna-mainnet-v1");
assert_ne!(MAINNET_CANDIDATE_NETWORK_ID, MAINNET_NETWORK_ID);
let dir = tempdir().unwrap();
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -82,6 +82,7 @@ window.iunaApp = function iunaApp() {
importSeedPhrase: "",
walletVerified: false,
setupFeedback: null,
+ migrationBusy: false,
burnAmount: 100,
burnAmountDraft: "0.0001",
burnFee: 1,
@@ -281,7 +282,38 @@ window.iunaApp = function iunaApp() {
},
showingSetup() {
- return this.authLoaded && !this.showingAuth() && !this.config.setup_complete;
+ return this.authLoaded && !this.showingAuth() && !this.showingNetworkMigration() && !this.config.setup_complete;
+ },
+
+ showingNetworkMigration() {
+ return this.authLoaded && !this.showingAuth() && this.status.network_migration?.required === true;
+ },
+
+ migrationNetworkLabel() {
+ return this.status.network_migration?.to_network || "the new Iuna network";
+ },
+
+ async finishNetworkMigration(setUpDifferentWallet) {
+ this.migrationBusy = true;
+ try {
+ await this.submitForm("/api/settings/chain-reset", { confirm: "RESET" });
+ if (setUpDifferentWallet) {
+ const response = await this.fetchWithTimeout("/api/config", {
+ method: "POST",
+ headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
+ body: new URLSearchParams({ setup_complete: "false", peer: "" }),
+ });
+ const payload = await response.json();
+ if (!response.ok || !payload.ok) throw new Error(payload.error || "Could not open wallet setup");
+ this.config.setup_complete = false;
+ }
+ await this.refresh({ force: true });
+ this.showFlash(setUpDifferentWallet ? "Local chain reset. Choose or import a wallet." : "Local chain reset. Sync requested from peers.", "success");
+ } catch (error) {
+ this.showFlash(error.message, "error");
+ } finally {
+ this.migrationBusy = false;
+ }
},
showingAuth() {
@@ -689,7 +721,7 @@ window.iunaApp = function iunaApp() {
if (!this.allowedTabs().includes(this.tab)) {
this.setTab("wallet");
}
- if (!this.config.setup_complete) {
+ if (!this.config.setup_complete || status.network_migration?.required === true) {
await this.refreshWalletSetup();
}
this.syncMempoolBlockMarker(previousChainHeight, status.chain?.height);
@@ -3018,6 +3050,8 @@ window.iunaApp = function iunaApp() {
syncingNode() {
return (
this.canUseProtectedApi() &&
+ !this.showingNetworkMigration() &&
+ !this.chainResetModalOpen &&
this.config.setup_complete === true &&
this.networkHealthLoaded &&
this.networkHealth.state === "syncing"