iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 3a9035707698cae295f928e278ed7582a8cbd35b
parent e054aa0bc325cac525fce26ab33a6a01cd0e5c53
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Mon, 31 Aug 2026 21:26:41 +0200

Improve transfer confirmation and address labels

Diffstat:
Msrc/adapters/http/index_html.rs | 9++++++++-
Msrc/main_tests.rs | 13+++++++++++++
Mwww/assets/iuna-ui.js | 53+++++++++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 70 insertions(+), 5 deletions(-)

diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs @@ -617,7 +617,6 @@ pub(super) const INDEX_HTML: &str = concat!( <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3H20v18H6.5A2.5 2.5 0 0 1 4 18.5z"></path><path d="M8 7h8"></path><path d="M8 11h6"></path><path d="M8 15h4"></path></svg> </button> </div> - <div class="fee-preview" x-show="transferTo.trim()">Verify recipient: <code x-text="transferTo.trim()"></code></div> <div class="amount-field"> <label>Amount<input x-model="transferAmount" @input="scheduleFeeEstimates" type="number" min="0.000001" step="0.000001" required></label> <button class="amount-max-button" type="button" @click="setMaxTransferAmount" :disabled="transferMaxDisabled()" title="Use maximum spendable amount">Max</button> @@ -1402,6 +1401,14 @@ pub(super) const INDEX_HTML: &str = concat!( </div> <div class="info-fact"> <div class="label">Fee / byte</div> + <div class="value">IUNA <span x-text="amountLabel(pendingTransfer?.feePerByte || 0)"></span></div> + </div> + <div class="info-fact"> + <div class="label">Transaction size</div> + <div class="value"><span x-text="pendingTransfer?.bytes || 0"></span> bytes</div> + </div> + <div class="info-fact"> + <div class="label">Network fee</div> <div class="value">IUNA <span x-text="amountLabel(pendingTransfer?.fee || 0)"></span></div> </div> </div> diff --git a/src/main_tests.rs b/src/main_tests.rs @@ -102,12 +102,25 @@ fn management_ui_uses_an_in_app_transfer_confirmation_and_marks_fee_errors() { let javascript = include_str!("../www/assets/iuna-ui.js"); assert!(html.contains("Confirm transfer")); assert!(html.contains("@click=\"confirmTransfer\"")); + assert!(html.contains("Transaction size")); + assert!(html.contains("Network fee")); + assert!(!html.contains("Verify recipient")); assert!(html.contains("error: feeEstimateError('transfer')")); + assert!(javascript.contains("bytes: Number(estimate.bytes)")); + assert!(javascript.contains("fee: this.microiunaAmount(estimate.fee)")); + assert!(javascript.contains("fee_per_byte: transfer.feePerByte")); assert!(javascript.contains("async confirmTransfer()")); assert!(!javascript.contains("window.confirm")); } #[test] +fn management_ui_matches_bech32m_contacts_to_canonical_transaction_addresses() { + let javascript = include_str!("../www/assets/iuna-ui.js"); + assert!(javascript.contains("canonicalAddressKey(address)")); + assert!(javascript.contains("this.canonicalAddressKey(savedAddress) === canonical")); +} + +#[test] fn management_ui_blocks_interaction_while_the_node_is_syncing() { let html = include_str!("adapters/http/index_html.rs"); let javascript = include_str!("../www/assets/iuna-ui.js"); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -2384,7 +2384,9 @@ window.iunaApp = function iunaApp() { this.pendingTransfer = { recipient: fullRecipient, amount, - fee, + feePerByte: fee, + bytes: Number(estimate.bytes), + fee: this.microiunaAmount(estimate.fee), utxos: this.selectedTransferUtxos.join("\n"), }; this.sendConfirmModalOpen = true; @@ -2412,7 +2414,7 @@ window.iunaApp = function iunaApp() { { to: transfer.recipient, amount: transfer.amount, - fee_per_byte: transfer.fee, + fee_per_byte: transfer.feePerByte, utxos: transfer.utxos, }, `Queued transfer of ${this.amountLabel(transfer.amount)} IUNA to ${recipient}` @@ -2503,7 +2505,12 @@ window.iunaApp = function iunaApp() { fee_per_byte: this.parseiunaAmount(this.transferFee), utxos: this.selectedTransferUtxos.join("\n"), }); - if (estimate?.error || !Number.isFinite(Number(estimate?.fee))) { + if ( + estimate?.error + || !Number.isFinite(Number(estimate?.fee)) + || !Number.isInteger(Number(estimate?.bytes)) + || Number(estimate.bytes) <= 0 + ) { throw new Error(estimate?.error || "Could not estimate transfer fee"); } return estimate; @@ -2671,9 +2678,47 @@ window.iunaApp = function iunaApp() { return `${value.slice(0, 8)}...${value.slice(-8)}`; }, + canonicalAddressKey(address) { + const normalized = String(address ?? "").trim().toLowerCase(); + if (/^[0-9a-f]{64}$/.test(normalized)) return normalized; + + const separator = normalized.lastIndexOf("1"); + const hrp = normalized.slice(0, separator); + if (separator <= 0 || (hrp !== "iuna" && hrp !== "tiuna")) return normalized; + + const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + const encoded = normalized.slice(separator + 1); + if (encoded.length < 7) return normalized; + const payload = [...encoded.slice(0, -6)].map((character) => charset.indexOf(character)); + if (payload.length === 0 || payload[0] !== 0 || payload.some((value) => value < 0)) return normalized; + + let accumulator = 0; + let bits = 0; + const bytes = []; + for (const value of payload.slice(1)) { + accumulator = (accumulator << 5) | value; + bits += 5; + while (bits >= 8) { + bits -= 8; + bytes.push((accumulator >> bits) & 0xff); + accumulator &= (1 << bits) - 1; + } + } + if (bits >= 5 || accumulator !== 0 || bytes.length !== 32) return normalized; + return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""); + }, + addressName(address) { if (!address) return null; - return this.addressBook?.[address] || null; + const normalized = String(address).trim().toLowerCase(); + const directName = this.addressBook?.[normalized]; + if (directName) return directName; + + const canonical = this.canonicalAddressKey(normalized); + for (const [savedAddress, name] of Object.entries(this.addressBook || {})) { + if (this.canonicalAddressKey(savedAddress) === canonical) return name; + } + return null; }, addressLabel(address) {