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:
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) {