iuna

iuna

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

commit 5366c29c04720a97d6f48b6f20d43fadfd757a59
parent 8c7f36d53434a15272933e380610f5947b0e534d
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue,  8 Sep 2026 21:58:31 +0200

fix(wallet): explain oversized transactions

Diffstat:
Mtests/lightweight-wallet.test.cjs | 23+++++++++++++++++++++++
Mwallet/app.js | 2+-
Mwallet/wallet-core.js | 31++++++++++++++++++++++---------
3 files changed, 46 insertions(+), 10 deletions(-)

diff --git a/tests/lightweight-wallet.test.cjs b/tests/lightweight-wallet.test.cjs @@ -41,3 +41,26 @@ test('refuses to sign with a watch-only wallet', async () => { /watch-only wallet cannot sign/, ); }); + +test('explains oversized transactions before submission', () => { + const signature = 'aa'.repeat(64); + const transaction = { + kind: 'transfer', + inputs: Array.from({ length: 205 }, (_, index) => ({ + outpoint: { txid: 'bb'.repeat(32), index }, + owner: 'cc'.repeat(32), + signature, + })), + outputs: [ + { address: 'dd'.repeat(32), amount: 1 }, + { address: 'cc'.repeat(32), amount: 1 }, + ], + fee: 1, + signature, + }; + + assert.throws( + () => core.ensureTransactionBodySize(transaction), + /205 inputs, 65 KiB; maximum 64 KiB.*smaller amount.*consolidate/s, + ); +}); diff --git a/wallet/app.js b/wallet/app.js @@ -30,7 +30,7 @@ function toast(message, error = false) { toastElement.textContent = message; toastElement.className = `toast show${error ? " error" : ""}`; window.clearTimeout(toastElement.timeout); - toastElement.timeout = window.setTimeout(() => { toastElement.className = "toast"; }, 3000); + toastElement.timeout = window.setTimeout(() => { toastElement.className = "toast"; }, error ? 7000 : 3000); } function readJson(key) { diff --git a/wallet/wallet-core.js b/wallet/wallet-core.js @@ -4,6 +4,7 @@ export const API_BASE = "https://iuna.jhx.app/v1"; export const STORAGE_KEY = "iuna.wallets.v2"; export const LEGACY_STORAGE_KEY = "iuna.wallet.v1"; export const MICRO_IUNA = 1_000_000n; +export const MAX_TRANSACTION_BODY_BYTES = 64 * 1024; export function emptyWalletStore() { return { version: 2, activeId: null, wallets: [] }; @@ -50,6 +51,16 @@ export function removeWallet(store, id) { return { version: 2, activeId: wallets[0]?.id || null, wallets }; } +export function ensureTransactionBodySize(transaction) { + const bodyBytes = encoder.encode(JSON.stringify(transaction)).byteLength; + if (bodyBytes > MAX_TRANSACTION_BODY_BYTES) { + const inputCount = transaction.inputs?.length || 0; + const bodyKiB = Math.ceil(bodyBytes / 1024); + throw new Error(`Transaction is too large (${inputCount} inputs, ${bodyKiB} KiB; maximum 64 KiB). Try a smaller amount or consolidate this wallet’s UTXOs first.`); + } + return bodyBytes; +} + export function bytesToHex(bytes) { return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); } @@ -304,16 +315,15 @@ export async function buildSignedTransfer({ wallet, status, utxos, recipientAddr ? transferSigningBytes(status, inputs, outputs, fee) : encoder.encode(legacyTransferPayload(inputs, outputs, fee)); const signature = bytesToHex(new Uint8Array(await crypto.subtle.sign("Ed25519", wallet.privateKey, signingPayload))); - return { - transaction: { - kind: "transfer", - inputs: inputs.map((input) => ({ ...input, signature })), - outputs: outputs.map((output) => ({ ...output, amount: Number(output.amount) })), - fee: Number(fee), - signature, - }, - fee, + const transaction = { + kind: "transfer", + inputs: inputs.map((input) => ({ ...input, signature })), + outputs: outputs.map((output) => ({ ...output, amount: Number(output.amount) })), + fee: Number(fee), + signature, }; + ensureTransactionBodySize(transaction); + return { transaction, fee }; } export function parseIuna(value) { @@ -353,6 +363,9 @@ export async function api(path, options = {}) { } let body; try { body = await response.json(); } catch { body = {}; } + if (response.status === 413 && path === "/transactions" && requestOptions.method === "POST") { + throw new Error("Transaction is too large for the public endpoint. Try a smaller amount or consolidate this wallet’s UTXOs first."); + } if (!response.ok) throw new Error(body.error || `Endpoint returned status ${response.status}`); return body; }