iuna

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

commit 3800ffbeebb24991b3031db2d201bf04d6a62794
parent e0ecaced5986cdc07ff47df7614d67f57abf10a7
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Mon, 10 Aug 2026 20:05:22 +0200

Add UI-configurable P2P bind port

Diffstat:
Msrc/adapters/config_store.rs | 15+++++++++++++++
Msrc/adapters/http.rs | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/adapters/p2p.rs | 4++++
Msrc/main.rs | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mwww/assets/iuna-ui.js | 54+++++++++++++++++++++++++++++++++++++++++++++++++++---
5 files changed, 218 insertions(+), 26 deletions(-)

diff --git a/src/adapters/config_store.rs b/src/adapters/config_store.rs @@ -20,6 +20,7 @@ pub const DEFAULT_BURN_FEE: Amount = DEFAULT_BURN_AMOUNT; pub const DEFAULT_RECOVERY_VDF_TOP_RANK_PERCENT: u8 = 50; pub const DEFAULT_POW_MINING_WORKERS: u8 = 1; pub const MAX_POW_MINING_WORKERS: u8 = 32; +pub const DEFAULT_P2P_BIND_PORT: u16 = 9444; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct UiConfig { @@ -34,6 +35,7 @@ pub struct UiConfig { pub recovery_vdf_top_rank_percent: u8, pub keep_track_of_metrics: bool, pub p2p_accept_inbound: bool, + pub p2p_bind_port: u16, pub p2p_announce_addr: Option<String>, pub peers: Vec<String>, pub address_book: BTreeMap<String, String>, @@ -52,6 +54,7 @@ impl Default for UiConfig { recovery_vdf_top_rank_percent: DEFAULT_RECOVERY_VDF_TOP_RANK_PERCENT, keep_track_of_metrics: false, p2p_accept_inbound: false, + p2p_bind_port: DEFAULT_P2P_BIND_PORT, p2p_announce_addr: None, peers: Vec::new(), address_book: BTreeMap::new(), @@ -83,6 +86,8 @@ struct ConfigFile { keep_track_of_metrics: bool, #[serde(default)] p2p_accept_inbound: Option<bool>, + #[serde(default = "default_p2p_bind_port")] + p2p_bind_port: u16, #[serde(default)] p2p_announce_addr: Option<String>, #[serde(default)] @@ -120,6 +125,7 @@ pub fn save(path: &Path, config: &UiConfig) -> Result<()> { recovery_vdf_top_rank_percent: Some(config.recovery_vdf_top_rank_percent), keep_track_of_metrics: config.keep_track_of_metrics, p2p_accept_inbound: Some(config.p2p_accept_inbound), + p2p_bind_port: config.p2p_bind_port, p2p_announce_addr: config.p2p_announce_addr.clone(), peers: config.peers.clone(), address_book: config.address_book.clone(), @@ -177,6 +183,7 @@ fn load(path: &Path) -> Result<UiConfig> { .min(100), keep_track_of_metrics: stored.keep_track_of_metrics, p2p_accept_inbound, + p2p_bind_port: stored.p2p_bind_port, p2p_announce_addr: stored.p2p_announce_addr, peers: stored.peers, address_book: stored.address_book, @@ -191,6 +198,10 @@ fn default_pow_mining_workers() -> u8 { DEFAULT_POW_MINING_WORKERS } +fn default_p2p_bind_port() -> u16 { + DEFAULT_P2P_BIND_PORT +} + fn create_config_file(path: &Path) -> Result<File> { let mut options = OpenOptions::new(); options.write(true).create(true).truncate(true); @@ -233,6 +244,7 @@ mod tests { assert!(!stored.contains("required_burn")); assert!(stored.contains("\"keep_track_of_metrics\": false")); assert!(stored.contains("\"p2p_accept_inbound\": false")); + assert!(stored.contains("\"p2p_bind_port\": 9444")); assert!(stored.contains("\"p2p_announce_addr\": null")); assert!(stored.contains("\"peers\": []")); assert!(stored.contains("\"address_book\": {}")); @@ -255,6 +267,7 @@ mod tests { burn_fee: 3 * MICRO_IUNA, keep_track_of_metrics: true, p2p_accept_inbound: true, + p2p_bind_port: 9555, p2p_announce_addr: Some("203.0.113.10:9444".to_string()), peers: vec!["127.0.0.1:9444".to_string()], address_book: [("iuna-address".to_string(), "Alice".to_string())].into(), @@ -273,6 +286,7 @@ mod tests { assert_eq!(config.burn_fee, 3 * MICRO_IUNA); assert!(config.keep_track_of_metrics); assert!(config.p2p_accept_inbound); + assert_eq!(config.p2p_bind_port, 9555); assert_eq!( config.p2p_announce_addr.as_deref(), Some("203.0.113.10:9444") @@ -321,6 +335,7 @@ mod tests { assert_eq!(config.burn_fee, DEFAULT_BURN_FEE); assert!(!config.keep_track_of_metrics); assert!(!config.p2p_accept_inbound); + assert_eq!(config.p2p_bind_port, 9444); assert_eq!(config.peers, vec!["127.0.0.1:9444"]); assert!(config.address_book.is_empty()); } diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -169,6 +169,15 @@ struct P2pAnnounceForm { #[derive(Debug, Deserialize)] struct P2pInboundForm { enabled: bool, + bind_port: Option<u16>, +} + +#[derive(Debug, Serialize)] +struct ConfigResponse { + #[serde(flatten)] + config: UiConfig, + p2p_inbound_runtime_active: bool, + p2p_runtime_bind_addr: String, } #[derive(Debug, Deserialize)] @@ -851,8 +860,12 @@ async fn api_blocks( Json(ui_blocks(blocks, &snapshot, &pending, &burn_leader_ranks)) } -async fn api_config(State(state): State<HttpState>) -> Json<UiConfig> { - Json(state.ui_config.lock().await.clone()) +async fn api_config(State(state): State<HttpState>) -> Json<ConfigResponse> { + Json(ConfigResponse { + config: state.ui_config.lock().await.clone(), + p2p_inbound_runtime_active: state.gossip.accepts_inbound().await, + p2p_runtime_bind_addr: state.gossip.listen_addr().to_string(), + }) } async fn api_wallet_setup( @@ -1152,7 +1165,7 @@ async fn api_p2p_inbound_form( State(state): State<HttpState>, Form(form): Form<P2pInboundForm>, ) -> Json<ActionResponse> { - action_json(set_p2p_accept_inbound(&state, form.enabled).await) + action_json(set_p2p_accept_inbound(&state, form.enabled, form.bind_port).await) } async fn burn_per_block_form( @@ -1346,15 +1359,24 @@ async fn set_p2p_announce_addr(state: &HttpState, addr: String) -> Result<()> { Ok(()) } -async fn set_p2p_accept_inbound(state: &HttpState, enabled: bool) -> Result<()> { +async fn set_p2p_accept_inbound( + state: &HttpState, + enabled: bool, + bind_port: Option<u16>, +) -> Result<()> { + let bind_port = bind_port.unwrap_or(config_store::DEFAULT_P2P_BIND_PORT); + if bind_port == 0 { + bail!("P2P bind port must be between 1 and 65535"); + } let previous = state.gossip.accepts_inbound().await; - if enabled { + if enabled && previous { state.gossip.set_accept_inbound(true).await?; } let mut config = state.ui_config.lock().await; let mut next_config = config.clone(); next_config.p2p_accept_inbound = enabled; + next_config.p2p_bind_port = bind_port; if let Err(error) = config_store::save(&state.config_path, &next_config) { let _ = state.gossip.set_accept_inbound(previous).await; return Err(error); @@ -3242,6 +3264,7 @@ const INDEX_HTML: &str = r#"<!doctype html> .flash { position: fixed; top: 18px; right: 18px; z-index: 80; width: min(420px, calc(100vw - 36px)); border-radius: 6px; padding: 10px 12px; border: 1px solid; font-weight: 700; box-shadow: 0 18px 48px rgba(0, 0, 0, .38); } .flash.success { color: #d5f55f; background: #1c2516; border-color: #566d25; } .flash.error { color: #ffb1a8; background: #2a1717; border-color: #713434; } + .persistent-banner { border: 1px solid #566d25; border-radius: 8px; padding: 10px 12px; margin: -4px 0 16px; color: #d5f55f; background: #1c2516; font-weight: 800; } .ok { color: #d5f55f; } .page-title { margin-bottom: 16px; } .setup-overlay { position: fixed; inset: 0; z-index: 30; display: grid; place-items: center; padding: 22px; background: rgba(8, 9, 10, .72); backdrop-filter: blur(8px); } @@ -3633,6 +3656,7 @@ const INDEX_HTML: &str = r#"<!doctype html> </header> <div class="flash" :class="flash?.kind" x-show="flash" x-transition x-text="flash?.message"></div> + <div class="persistent-banner" x-show="p2pRestartRequired()" x-transition x-text="p2pRestartMessage()"></div> <section x-show="tab === 'wallet'"> <div class="page-title"> @@ -4257,8 +4281,9 @@ const INDEX_HTML: &str = r#"<!doctype html> </label> </div> <form class="settings-form public-p2p-form" x-show="p2pAcceptInbound" x-transition @submit.prevent="saveP2pAnnounce"> + <label>Bind port<input x-model.number="p2pBindPort" @input="p2pBindPortDirty = true" type="number" min="1" max="65535" step="1" required></label> <label>Public P2P address<input x-model="p2pAnnounceAddr" @input="p2pAnnounceDirty = true" placeholder="203.0.113.10:9444"></label> - <div class="muted">Use this only when TCP port 9444 is reachable from the internet.</div> + <div class="muted">Use this only when TCP port <span x-text="p2pBindPort"></span> is reachable from the internet.</div> <div class="setup-actions"><button class="primary" type="submit">Save</button></div> </form> </div> @@ -4525,6 +4550,7 @@ const INDEX_HTML: &str = r#"<!doctype html> </div> <div class="setup-network-row"> <label><span x-text="setupRequiresPeer() ? 'Bootstrap peer (required)' : 'Bootstrap peer'"></span><input x-model="setupPeerAddress" placeholder="iuna.jhx.app:9444"></label> + <label x-show="setupNodeMode === 'listening'" x-transition>Bind port<input x-model.number="p2pBindPort" @input="p2pBindPortDirty = true" type="number" min="1" max="65535" step="1" required></label> </div> <div class="setup-network-copy" x-text="setupRequiresPeer() ? 'A bootstrap peer is required before this node can join the network. Known nodes help discovery; they do not control your wallet or decide valid blocks.' : 'You can add a bootstrap peer now or later from the P2P screen. Known nodes help discovery; they do not control your wallet or decide valid blocks.'"></div> </div> @@ -5371,18 +5397,23 @@ mod tests { other => panic!("expected peer list, got {other:?}"), } - super::set_p2p_accept_inbound(&state, true).await.unwrap(); + super::set_p2p_accept_inbound(&state, true, Some(9555)) + .await + .unwrap(); let config = config_store::load_or_create(&config_path).unwrap(); assert!(config.p2p_accept_inbound); - assert!(state.gossip.accepts_inbound().await); + assert_eq!(config.p2p_bind_port, 9555); + assert!(!state.gossip.accepts_inbound().await); match state.gossip.peer_exchange().await { GossipEnvelope::PeerList { peers } => { - assert!(peers.contains(&"203.0.113.10:9444".to_string())); + assert!(!peers.contains(&"203.0.113.10:9444".to_string())); } other => panic!("expected peer list, got {other:?}"), } - super::set_p2p_accept_inbound(&state, false).await.unwrap(); + super::set_p2p_accept_inbound(&state, false, None) + .await + .unwrap(); let config = config_store::load_or_create(&config_path).unwrap(); assert!(!config.p2p_accept_inbound); assert!(!state.gossip.accepts_inbound().await); @@ -6154,6 +6185,8 @@ mod tests { assert!(super::INDEX_HTML.contains("selectSetupNodeMode('wallet')")); assert!(super::INDEX_HTML.contains("selectSetupNodeMode('non-listening')")); assert!(super::INDEX_HTML.contains("selectSetupNodeMode('listening')")); + assert!(super::INDEX_HTML.contains("setupNodeMode === 'listening'")); + assert!(super::INDEX_HTML.contains("x-model.number=\"p2pBindPort\"")); assert!(super::INDEX_HTML.contains("Change later in Settings")); } @@ -6177,10 +6210,24 @@ mod tests { assert!(app_js.contains("async applySetupNodeMode()")); assert!(app_js.contains("await this.applySetupNodeMode();")); assert!(app_js.contains("\"/api/settings/p2p-inbound\"")); + assert!(app_js.contains("bind_port: this.p2pBindPortValue()")); assert!(app_js.contains("this.setUiMode(mode === \"wallet\" ? \"basic\" : \"advanced\")")); } #[test] + fn p2p_bind_port_changes_show_global_restart_notice() { + let app_js = include_str!("../../www/assets/iuna-ui.js"); + assert!(super::INDEX_HTML.contains("Bind port")); + assert!(super::INDEX_HTML.contains("persistent-banner")); + assert!(super::INDEX_HTML.contains("p2pRestartRequired()")); + assert!(app_js.contains("p2pBindPort: 9444")); + assert!(app_js.contains("p2pConfiguredBindAddr()")); + assert!(app_js.contains("p2pRestartMessage()")); + assert!(app_js.contains("Restart iuna to close the public P2P listener.")); + assert!(app_js.contains("0.0.0.0:${port}")); + } + + #[test] fn polling_refreshes_paged_datasets_without_visible_loaders() { let app_js = include_str!("../../www/assets/iuna-ui.js"); assert!(app_js.contains("setInterval(() => this.refresh({ silent: true }), 5000)")); diff --git a/src/adapters/p2p.rs b/src/adapters/p2p.rs @@ -431,6 +431,10 @@ impl GossipNetwork { self.inner.accept_task.lock().await.is_some() } + pub fn listen_addr(&self) -> SocketAddr { + self.inner.listen_addr + } + pub async fn set_p2p_announce_addr(&self, addr: Option<SocketAddr>) { *self.inner.p2p_announce_addr.lock().await = addr; } diff --git a/src/main.rs b/src/main.rs @@ -1,6 +1,6 @@ use std::{ collections::BTreeMap, - net::SocketAddr, + net::{Ipv4Addr, SocketAddr}, path::{Path, PathBuf}, str::FromStr, sync::Arc, @@ -48,13 +48,11 @@ async fn main() -> Result<()> { ); } let mut ui_config = config_store::load_or_create(&config_path)?; + let ui_config_dirty = apply_cli_p2p_config_overrides(&opts, &mut ui_config); let p2p_announce_addr = configured_p2p_announce_addr(&opts, &ui_config)?; - let p2p_accept_inbound = opts.p2p_announce_addr.is_some() || ui_config.p2p_accept_inbound; - if let Some(addr) = opts.p2p_announce_addr { - ui_config.p2p_accept_inbound = true; - ui_config.p2p_announce_addr = Some(addr.to_string()); - } - let advertised_p2p_addr = p2p_announce_addr.unwrap_or(opts.p2p_addr); + let configured_p2p_addr = configured_p2p_bind_addr(&opts, &ui_config); + let p2p_accept_inbound = ui_config.p2p_accept_inbound; + let advertised_p2p_addr = p2p_announce_addr.unwrap_or(configured_p2p_addr); let wallet_load = load_startup_wallet(&wallet_path)?; let wallet_address = wallet_load.address().to_string(); if opts.chain_mode == ChainMode::Genesis { @@ -64,6 +62,8 @@ async fn main() -> Result<()> { ui_config.burn_per_block = GENESIS_INITIAL_BURN_PER_BLOCK; ui_config.burn_fee = GENESIS_INITIAL_BURN_FEE; config_store::save(&config_path, &ui_config)?; + } else if ui_config_dirty { + config_store::save(&config_path, &ui_config)?; } let ledger = initialize_ledger(&opts, &wallet_address, &chain_store, advertised_p2p_addr).await?; @@ -121,7 +121,7 @@ async fn main() -> Result<()> { println!("chain database: {}", chain_store.path().display()); println!("management UI: http://{}", opts.http_addr); if p2p_accept_inbound { - println!("p2p listener: {}", opts.p2p_addr); + println!("p2p listener: {}", configured_p2p_addr); } else { println!("p2p listener: disabled (outbound-only)"); } @@ -139,7 +139,7 @@ async fn main() -> Result<()> { let gossip = p2p::GossipNetwork::start( Arc::clone(&node), Arc::clone(&peers), - opts.p2p_addr, + configured_p2p_addr, p2p_announce_addr, p2p_accept_inbound, ) @@ -312,6 +312,38 @@ fn configured_p2p_announce_addr( .transpose() } +fn configured_p2p_bind_addr(opts: &CliOptions, ui_config: &config_store::UiConfig) -> SocketAddr { + if opts.p2p_addr_configured || ui_config.p2p_accept_inbound { + return SocketAddr::from((Ipv4Addr::UNSPECIFIED, ui_config.p2p_bind_port)); + } + opts.p2p_addr +} + +fn apply_cli_p2p_config_overrides( + opts: &CliOptions, + ui_config: &mut config_store::UiConfig, +) -> bool { + let mut dirty = false; + if opts.p2p_addr_configured { + let bind_port = opts.p2p_addr.port(); + if ui_config.p2p_bind_port != bind_port { + ui_config.p2p_bind_port = bind_port; + dirty = true; + } + } + if let Some(addr) = opts.p2p_announce_addr { + let announce_addr = addr.to_string(); + if !ui_config.p2p_accept_inbound + || ui_config.p2p_announce_addr.as_deref() != Some(&announce_addr) + { + ui_config.p2p_accept_inbound = true; + ui_config.p2p_announce_addr = Some(announce_addr); + dirty = true; + } + } + dirty +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ChainMode { Setup, @@ -325,6 +357,7 @@ struct CliOptions { chain_db_path: Option<PathBuf>, http_addr: SocketAddr, p2p_addr: SocketAddr, + p2p_addr_configured: bool, p2p_announce_addr: Option<SocketAddr>, stratum_addr: Option<SocketAddr>, peers: Vec<String>, @@ -345,6 +378,7 @@ impl CliOptions { chain_db_path: None, http_addr: SocketAddr::from_str("127.0.0.1:18661")?, p2p_addr: SocketAddr::from_str("127.0.0.1:9444")?, + p2p_addr_configured: false, p2p_announce_addr: None, stratum_addr: None, peers: Vec::new(), @@ -384,6 +418,7 @@ impl CliOptions { opts.p2p_addr = next_value(&mut args, "--p2p")? .parse() .context("invalid --p2p address")?; + opts.p2p_addr_configured = true; } "--p2p-announce" => { opts.p2p_announce_addr = Some( @@ -844,10 +879,10 @@ mod tests { use super::{ ChainMode, CliOptions, GENESIS_INITIAL_BURN_FEE, GENESIS_INITIAL_BURN_PER_BLOCK, - StartupWallet, configured_p2p_announce_addr, extrapolate_vdf_rounds, help_text, - initial_burn_fee, initial_burn_per_block, initialize_ledger, load_startup_wallet, - measure_vdf_rounds, persist_chain_snapshot, run_chain_persistence_with_interval, - validate_wallet_for_mode, + StartupWallet, apply_cli_p2p_config_overrides, configured_p2p_announce_addr, + configured_p2p_bind_addr, extrapolate_vdf_rounds, help_text, initial_burn_fee, + initial_burn_per_block, initialize_ledger, load_startup_wallet, measure_vdf_rounds, + persist_chain_snapshot, run_chain_persistence_with_interval, validate_wallet_for_mode, }; fn parse(args: &[&str]) -> anyhow::Result<Option<CliOptions>> { @@ -1119,6 +1154,49 @@ mod tests { } #[test] + fn configured_p2p_bind_addr_uses_configured_public_port() { + let opts = parse(&[]).unwrap().unwrap(); + let config = UiConfig { + p2p_accept_inbound: true, + p2p_bind_port: 9555, + ..UiConfig::default() + }; + + assert_eq!( + configured_p2p_bind_addr(&opts, &config).to_string(), + "0.0.0.0:9555" + ); + } + + #[test] + fn configured_p2p_bind_addr_uses_cli_port_after_config_override() { + let opts = parse(&["--p2p", "127.0.0.1:9555"]).unwrap().unwrap(); + let config = UiConfig { + p2p_accept_inbound: true, + p2p_bind_port: 9555, + ..UiConfig::default() + }; + + assert_eq!( + configured_p2p_bind_addr(&opts, &config).to_string(), + "0.0.0.0:9555" + ); + } + + #[test] + fn cli_p2p_port_overrides_config_bind_port() { + let opts = parse(&["--p2p", "127.0.0.1:9555"]).unwrap().unwrap(); + let mut config = UiConfig { + p2p_accept_inbound: true, + p2p_bind_port: 9444, + ..UiConfig::default() + }; + + assert!(apply_cli_p2p_config_overrides(&opts, &mut config)); + assert_eq!(config.p2p_bind_port, 9555); + } + + #[test] fn http_management_port_defaults_to_iuna_port() { let opts = parse(&[]).unwrap().unwrap(); assert_eq!(opts.http_addr.to_string(), "127.0.0.1:18661"); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -57,6 +57,8 @@ window.iunaApp = function iunaApp() { addressBookDraftAddress: "", addressBookDraftName: "", p2pAcceptInbound: false, + p2pBindPort: 9444, + p2pBindPortDirty: false, p2pAnnounceAddr: "", p2pAnnounceDirty: false, setupWallet: { address: null, seed_phrase: null, dev_verify_bypass: false, requires_peer: false }, @@ -396,6 +398,9 @@ window.iunaApp = function iunaApp() { this.recoveryVdfTopRankPercent ); this.p2pAcceptInbound = this.config.p2p_accept_inbound === true; + if (!this.p2pBindPortDirty) { + this.p2pBindPort = Number(this.config.p2p_bind_port || 9444); + } if ( options.addressBookVersion === undefined || options.addressBookVersion >= this.addressBookVersion @@ -584,7 +589,10 @@ window.iunaApp = function iunaApp() { : "wallet"; const acceptInbound = mode === "listening"; if (this.p2pAcceptInbound !== acceptInbound) { - await this.submitForm("/api/settings/p2p-inbound", { enabled: acceptInbound }); + await this.submitForm("/api/settings/p2p-inbound", { + enabled: acceptInbound, + bind_port: this.p2pBindPortValue(), + }); this.p2pAcceptInbound = acceptInbound; } this.setUiMode(mode === "wallet" ? "basic" : "advanced"); @@ -1387,9 +1395,10 @@ window.iunaApp = function iunaApp() { this.p2pAcceptInbound = enabled; await this.postForm( "/api/settings/p2p-inbound", - { enabled }, - enabled ? "Public node enabled" : "Switched to outbound-only P2P" + { enabled, bind_port: this.p2pBindPortValue() }, + enabled ? "Public node setting saved" : "Switched to outbound-only P2P" ); + this.p2pBindPortDirty = false; await this.refreshConfig(); } catch (error) { this.p2pAcceptInbound = previous; @@ -1397,6 +1406,37 @@ window.iunaApp = function iunaApp() { } }, + p2pBindPortValue() { + const port = Number(this.p2pBindPort); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new Error("P2P bind port must be between 1 and 65535"); + } + return port; + }, + + p2pConfiguredBindAddr() { + const port = Number(this.config.p2p_bind_port || 9444); + if (!Number.isInteger(port) || port < 1 || port > 65535) return null; + return `0.0.0.0:${port}`; + }, + + p2pRestartRequired() { + const runtimeActive = this.config.p2p_inbound_runtime_active === true; + if (this.p2pAcceptInbound !== runtimeActive) return true; + if (!this.p2pAcceptInbound) return false; + const configured = this.p2pConfiguredBindAddr(); + return configured ? this.config.p2p_runtime_bind_addr !== configured : false; + }, + + p2pRestartMessage() { + if (!this.p2pRestartRequired()) return ""; + if (!this.p2pAcceptInbound && this.config.p2p_inbound_runtime_active === true) { + return "Restart iuna to close the public P2P listener."; + } + const configured = this.p2pConfiguredBindAddr(); + return `Restart iuna to open public P2P on ${configured || "the configured bind port"}.`; + }, + async saveP2pAnnounce() { if (!this.p2pAcceptInbound) { this.showFlash("Enable public node before setting a public P2P address", "error"); @@ -1404,6 +1444,13 @@ window.iunaApp = function iunaApp() { } const addr = this.p2pAnnounceAddr.trim(); try { + if (this.p2pBindPortDirty) { + await this.submitForm("/api/settings/p2p-inbound", { + enabled: true, + bind_port: this.p2pBindPortValue(), + }); + this.p2pBindPortDirty = false; + } await this.postForm( "/api/settings/p2p-announce", { addr }, @@ -1411,6 +1458,7 @@ window.iunaApp = function iunaApp() { ); this.p2pAnnounceAddr = addr; this.p2pAnnounceDirty = false; + await this.refreshConfig(); } catch (error) { this.showFlash(error.message, "error"); }