iuna

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

commit aa495f7ba0ef30a3d55aea7ca8bdc0059a24252d
parent 0b0c67de04e04b7349c2eab60774d0369eaed1ce
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Mon, 27 Jul 2026 22:44:37 +0200

Add bootstrap peer setup

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
AKNOWN_NODES.txt | 11+++++++++++
Massets/iuna-ui.js | 8+++++++-
Msrc/adapters/http.rs | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
5 files changed, 73 insertions(+), 24 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -502,7 +502,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.1.0" +version = "0.1.4" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.1.0" +version = "0.1.4" edition = "2024" license = "Apache-2.0" diff --git a/KNOWN_NODES.txt b/KNOWN_NODES.txt @@ -0,0 +1,11 @@ +# Known iuna devnet nodes +# +# These are bootstrap peers, not authorities. You may run without them or use +# your own trusted peer. A bootstrap peer helps your node find the network; it +# does not control your wallet or decide valid blocks. +# +# Format: host:port +# +# Use the P2P listener port here, not the management UI port. + +iuna.jhx.app:9444 diff --git a/assets/iuna-ui.js b/assets/iuna-ui.js @@ -49,6 +49,7 @@ window.iunaApp = function iunaApp() { feeEstimateTimer: null, showSendAdvanced: false, selectedTransferUtxos: [], + setupPeerAddress: "", peerAddress: "", flash: null, flashTimer: null, @@ -361,16 +362,21 @@ window.iunaApp = function iunaApp() { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", }, - body: new URLSearchParams({ setup_complete: "true" }), + body: new URLSearchParams({ + setup_complete: "true", + peer: this.setupPeerAddress.trim(), + }), }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || `/api/config returned ${response.status}`); } await this.refreshConfig(); + this.peers = await this.fetchJson("/api/peers"); this.setupFeedback = null; this.generatedSeedPhrase = ""; this.importSeedPhrase = ""; + this.setupPeerAddress = ""; this.verifyChallenges = []; this.verifyAnswers = {}; this.showFlash("Setup complete", "success"); diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -126,6 +126,8 @@ struct PeerForm { #[derive(Debug, Deserialize)] struct ConfigForm { setup_complete: bool, + #[serde(default)] + peer: String, } #[derive(Debug, Deserialize)] @@ -543,9 +545,17 @@ async fn api_config_form( State(state): State<HttpState>, Form(form): Form<ConfigForm>, ) -> Json<ActionResponse> { + action_json(apply_config_form(&state, form).await) +} + +async fn apply_config_form(state: &HttpState, form: ConfigForm) -> Result<()> { + let peer = form.peer.trim(); + if !peer.is_empty() { + add_peer(state, peer.to_string()).await?; + } let mut config = state.ui_config.lock().await; config.setup_complete = form.setup_complete; - action_json(config_store::save(&state.config_path, &config)) + config_store::save(&state.config_path, &config) } async fn api_wallet_generate_form( @@ -1695,14 +1705,17 @@ const INDEX_HTML: &str = r#"<!doctype html> .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-network, .setup-wallet-section { grid-column: 1 / -1; } + .setup-network-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-items: end; } + .setup-network-copy { margin-top: 8px; color: #a8b2b8; line-height: 1.45; } + .setup-network-link { color: #d5f55f; font-size: 12px; font-weight: 900; text-decoration: none; } + .setup-network-link:hover { text-decoration: underline; } .setup-field { display: grid; gap: 6px; } .setup-field-label { color: #8d989f; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0; } .setup-address-box { display: flex; justify-content: space-between; gap: 10px; align-items: center; } .setup-address-box code { min-width: 0; } .setup-address-box button { flex: 0 0 auto; } .setup-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 14px; } - .setup-peer-list { margin-top: 14px; display: grid; gap: 8px; } - .setup-peer-row { display: flex; justify-content: space-between; gap: 12px; align-items: center; border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #181b1f; } .segmented { display: inline-flex; gap: 4px; padding: 4px; border: 1px solid #2f363c; border-radius: 8px; background: #181b1f; } .segmented button { border-color: transparent; background: transparent; color: #9fa8ad; } .segmented button.active { background: #d5f55f; color: #15171a; } @@ -2418,11 +2431,21 @@ const INDEX_HTML: &str = r#"<!doctype html> <div class="setup-modal-head"> <div class="setup-welcome">Welcome to iuna</div> <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 class="setup-copy">Choose how this node finds the network, then set up the local wallet.</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="setup-section setup-network"> + <div class="panel-head"> + <h3>Network</h3> + <a class="setup-network-link" href="https://github.com/iuna-labs/iuna/blob/main/KNOWN_NODES.txt" target="_blank" rel="noreferrer">Known nodes</a> + </div> + <div class="setup-network-row"> + <label>Bootstrap peer<input x-model="setupPeerAddress" placeholder="iuna.jhx.app:9444"></label> + </div> + <div class="setup-network-copy">Known nodes are bootstrap peers, not authorities. They help this node find the network; they do not control your wallet or decide valid blocks.</div> + </div> + <div class="setup-section setup-wallet-section seed-panel"> <div class="panel-head"> <h3>Wallet</h3> </div> @@ -2494,22 +2517,6 @@ const INDEX_HTML: &str = r#"<!doctype html> </template> </div> </div> - <div class="setup-section"> - <h3>Peers</h3> - <form @submit.prevent="addPeer"> - <label>Peer address<input x-model="peerAddress" placeholder="127.0.0.1:9445"></label> - <button class="primary" type="submit">Add</button> - </form> - <div class="setup-peer-list"> - <template x-for="peer in peers" :key="peer.address"> - <div class="setup-peer-row"> - <code x-text="peer.address"></code> - <span class="muted" x-text="peer.direction"></span> - </div> - </template> - <div class="muted" x-show="peers.length === 0">No peers</div> - </div> - </div> </div> <div class="setup-actions"> <button class="primary" type="button" :disabled="!walletVerified" @click="completeSetup">Continue</button> @@ -2714,6 +2721,31 @@ mod tests { } #[tokio::test] + async fn setup_config_form_can_add_bootstrap_peer() { + let dir = tempfile::tempdir().unwrap(); + let config_path = dir.path().join("config.json"); + let state = auth_test_state(config_path.clone(), UiConfig::default()).await; + + super::apply_config_form( + &state, + super::ConfigForm { + setup_complete: true, + peer: " iuna.jhx.app:9444 ".to_string(), + }, + ) + .await + .unwrap(); + + let config = config_store::load_or_create(&config_path).unwrap(); + assert!(config.setup_complete); + assert_eq!(config.peers, vec!["iuna.jhx.app:9444"]); + assert_eq!( + state.peers.lock().await.addresses(), + vec!["iuna.jhx.app:9444"] + ); + } + + #[tokio::test] async fn peer_management_rejects_empty_and_inbound_removal() { let dir = tempfile::tempdir().unwrap(); let state = auth_test_state(dir.path().join("config.json"), UiConfig::default()).await;